Skip to main content

pawan/agent/
events.rs

1//! Typed event stream for agent UI rendering.
2//!
3//! Provides a sealed enum of agent events that decouples the agent loop
4//! from TUI rendering. All rendering in pawan-cli subscribes to these events.
5
6use serde::{Deserialize, Serialize};
7
8/// A turn in the agent loop has started.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct TurnStartEvent {
11    /// The user prompt that started this turn
12    pub prompt: String,
13    /// Timestamp when the turn started (Unix epoch seconds)
14    pub timestamp_secs: u64,
15}
16
17/// The model is producing a thinking delta (partial content).
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ThinkingDeltaEvent {
20    /// The delta content
21    pub content: String,
22    /// Whether this is the first delta
23    pub is_first: bool,
24}
25
26/// A tool call was approved (either auto-approved or user-confirmed).
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ToolApprovalEvent {
29    /// Tool call ID
30    pub call_id: String,
31    /// Tool name
32    pub tool_name: String,
33    /// Arguments passed to the tool
34    pub arguments: serde_json::Value,
35    /// Whether this was auto-approved (read-only tool)
36    pub auto_approved: bool,
37}
38
39/// A tool has started executing.
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ToolStartEvent {
42    /// Tool call ID
43    pub call_id: String,
44    /// Tool name
45    pub tool_name: String,
46    /// Arguments passed to the tool
47    pub arguments: serde_json::Value,
48}
49
50/// A tool has completed execution.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ToolCompleteEvent {
53    /// Tool call ID
54    pub call_id: String,
55    /// Tool name
56    pub tool_name: String,
57    /// Result from the tool execution
58    pub result: String,
59    /// Whether the tool executed successfully
60    pub success: bool,
61    /// Duration in milliseconds
62    pub duration_ms: u64,
63}
64
65/// A turn in the agent loop has ended.
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct TurnEndEvent {
68    /// The final content from the model
69    pub content: String,
70    /// Number of tool calls in this turn
71    pub tool_call_count: usize,
72    /// Total iterations used
73    pub iterations: usize,
74    /// Token usage for this turn
75    pub usage: TokenUsageInfo,
76}
77
78/// Token usage information.
79#[derive(Debug, Clone, Serialize, Deserialize, Default)]
80pub struct TokenUsageInfo {
81    pub prompt_tokens: u64,
82    pub completion_tokens: u64,
83    pub total_tokens: u64,
84}
85
86/// The agent session has ended.
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct SessionEndEvent {
89    /// The final response content
90    pub content: String,
91    /// Total iterations used
92    pub total_iterations: usize,
93    /// Total token usage
94    pub usage: TokenUsageInfo,
95    /// Why the session ended
96    pub finish_reason: FinishReason,
97}
98
99/// Why the agent session ended.
100#[derive(Debug, Clone, Serialize, Deserialize)]
101#[serde(tag = "type", content = "message")]
102pub enum FinishReason {
103    /// The model produced a final response without needing more tool calls
104    Stop,
105    /// Hit the maximum iteration limit
106    MaxIterations,
107    /// An error occurred
108    Error(String),
109    /// An unknown tool was called
110    UnknownTool(String),
111    /// Session was cancelled by user
112    Cancelled,
113}
114
115/// A typed event emitted by the agent during execution.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117#[serde(tag = "event_type")]
118pub enum AgentEvent {
119    /// Turn started
120    TurnStart(TurnStartEvent),
121    /// Thinking delta (partial content)
122    ThinkingDelta(ThinkingDeltaEvent),
123    /// Tool call approved
124    ToolApproval(ToolApprovalEvent),
125    /// Tool started executing
126    ToolStart(ToolStartEvent),
127    /// Tool completed
128    ToolComplete(ToolCompleteEvent),
129    /// Turn ended
130    TurnEnd(TurnEndEvent),
131    /// Session ended
132    SessionEnd(SessionEndEvent),
133}
134
135impl AgentEvent {
136    /// Create a TurnStart event
137    pub fn turn_start(prompt: &str) -> Self {
138        AgentEvent::TurnStart(TurnStartEvent {
139            prompt: prompt.to_string(),
140            timestamp_secs: std::time::SystemTime::now()
141                .duration_since(std::time::UNIX_EPOCH)
142                .unwrap_or_default()
143                .as_secs(),
144        })
145    }
146
147    /// Create a ThinkingDelta event
148    pub fn thinking_delta(content: &str, is_first: bool) -> Self {
149        AgentEvent::ThinkingDelta(ThinkingDeltaEvent {
150            content: content.to_string(),
151            is_first,
152        })
153    }
154
155    /// Create a ToolApproval event
156    pub fn tool_approval(call_id: &str, tool_name: &str, arguments: serde_json::Value, auto_approved: bool) -> Self {
157        AgentEvent::ToolApproval(ToolApprovalEvent {
158            call_id: call_id.to_string(),
159            tool_name: tool_name.to_string(),
160            arguments,
161            auto_approved,
162        })
163    }
164
165    /// Create a ToolStart event
166    pub fn tool_start(call_id: &str, tool_name: &str, arguments: serde_json::Value) -> Self {
167        AgentEvent::ToolStart(ToolStartEvent {
168            call_id: call_id.to_string(),
169            tool_name: tool_name.to_string(),
170            arguments,
171        })
172    }
173
174    /// Create a ToolComplete event
175    pub fn tool_complete(call_id: &str, tool_name: &str, result: &str, success: bool, duration_ms: u64) -> Self {
176        AgentEvent::ToolComplete(ToolCompleteEvent {
177            call_id: call_id.to_string(),
178            tool_name: tool_name.to_string(),
179            result: result.to_string(),
180            success,
181            duration_ms,
182        })
183    }
184
185    /// Create a TurnEnd event
186    pub fn turn_end(content: &str, tool_call_count: usize, iterations: usize, usage: TokenUsageInfo) -> Self {
187        AgentEvent::TurnEnd(TurnEndEvent {
188            content: content.to_string(),
189            tool_call_count,
190            iterations,
191            usage,
192        })
193    }
194
195    /// Create a SessionEnd event
196    pub fn session_end(content: &str, total_iterations: usize, usage: TokenUsageInfo, reason: FinishReason) -> Self {
197        AgentEvent::SessionEnd(SessionEndEvent {
198            content: content.to_string(),
199            total_iterations,
200            usage,
201            finish_reason: reason,
202        })
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209
210    #[test]
211    fn test_turn_start_event() {
212        let event = AgentEvent::turn_start("Hello, world!");
213        assert!(matches!(event, AgentEvent::TurnStart(_)));
214    }
215
216    #[test]
217    fn test_thinking_delta_event() {
218        let event = AgentEvent::thinking_delta("Hello", true);
219        assert!(matches!(event, AgentEvent::ThinkingDelta(_)));
220    }
221
222    #[test]
223    fn test_tool_approval_event() {
224        let args = serde_json::json!({"command": "ls"});
225        let event = AgentEvent::tool_approval("call_1", "bash", args, true);
226        assert!(matches!(event, AgentEvent::ToolApproval(_)));
227    }
228
229    #[test]
230    fn test_tool_start_event() {
231        let args = serde_json::json!({"command": "ls"});
232        let event = AgentEvent::tool_start("call_1", "bash", args);
233        assert!(matches!(event, AgentEvent::ToolStart(_)));
234    }
235
236    #[test]
237    fn test_tool_complete_event() {
238        let event = AgentEvent::tool_complete("call_1", "bash", "file1\nfile2", true, 100);
239        assert!(matches!(event, AgentEvent::ToolComplete(_)));
240    }
241
242    #[test]
243    fn test_turn_end_event() {
244        let usage = TokenUsageInfo {
245            prompt_tokens: 100,
246            completion_tokens: 50,
247            total_tokens: 150,
248        };
249        let event = AgentEvent::turn_end("Done!", 2, 5, usage);
250        assert!(matches!(event, AgentEvent::TurnEnd(_)));
251    }
252
253    #[test]
254    fn test_session_end_event() {
255        let usage = TokenUsageInfo::default();
256        let event = AgentEvent::session_end("Goodbye!", 10, usage, FinishReason::Stop);
257        assert!(matches!(event, AgentEvent::SessionEnd(_)));
258    }
259
260    #[test]
261    fn test_finish_reason_variants() {
262        let stop = FinishReason::Stop;
263        let max_iter = FinishReason::MaxIterations;
264        let error = FinishReason::Error("something went wrong".to_string());
265        let unknown = FinishReason::UnknownTool("fake_tool".to_string());
266        let cancelled = FinishReason::Cancelled;
267
268        assert!(matches!(stop, FinishReason::Stop));
269        assert!(matches!(max_iter, FinishReason::MaxIterations));
270        assert!(matches!(error, FinishReason::Error(_)));
271        assert!(matches!(unknown, FinishReason::UnknownTool(_)));
272        assert!(matches!(cancelled, FinishReason::Cancelled));
273    }
274
275    #[test]
276    fn test_event_serialization() {
277        let event = AgentEvent::turn_start("test prompt");
278        let json = serde_json::to_string(&event).unwrap();
279        assert!(json.contains("TurnStart"));
280        assert!(json.contains("test prompt"));
281    }
282}