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/// Re-export canonical `FinishReason` from the coordinator.
100pub use crate::coordinator::FinishReason;
101
102/// A typed event emitted by the agent during execution.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104#[serde(tag = "event_type")]
105pub enum AgentEvent {
106    /// Turn started
107    TurnStart(TurnStartEvent),
108    /// Thinking delta (partial content)
109    ThinkingDelta(ThinkingDeltaEvent),
110    /// Tool call approved
111    ToolApproval(ToolApprovalEvent),
112    /// Tool started executing
113    ToolStart(ToolStartEvent),
114    /// Tool completed
115    ToolComplete(ToolCompleteEvent),
116    /// Turn ended
117    TurnEnd(TurnEndEvent),
118    /// Session ended
119    SessionEnd(SessionEndEvent),
120}
121
122impl AgentEvent {
123    /// Create a TurnStart event
124    pub fn turn_start(prompt: &str) -> Self {
125        AgentEvent::TurnStart(TurnStartEvent {
126            prompt: prompt.to_string(),
127            timestamp_secs: std::time::SystemTime::now()
128                .duration_since(std::time::UNIX_EPOCH)
129                .unwrap_or_default()
130                .as_secs(),
131        })
132    }
133
134    /// Create a ThinkingDelta event
135    pub fn thinking_delta(content: &str, is_first: bool) -> Self {
136        AgentEvent::ThinkingDelta(ThinkingDeltaEvent {
137            content: content.to_string(),
138            is_first,
139        })
140    }
141
142    /// Create a ToolApproval event
143    pub fn tool_approval(call_id: &str, tool_name: &str, arguments: serde_json::Value, auto_approved: bool) -> Self {
144        AgentEvent::ToolApproval(ToolApprovalEvent {
145            call_id: call_id.to_string(),
146            tool_name: tool_name.to_string(),
147            arguments,
148            auto_approved,
149        })
150    }
151
152    /// Create a ToolStart event
153    pub fn tool_start(call_id: &str, tool_name: &str, arguments: serde_json::Value) -> Self {
154        AgentEvent::ToolStart(ToolStartEvent {
155            call_id: call_id.to_string(),
156            tool_name: tool_name.to_string(),
157            arguments,
158        })
159    }
160
161    /// Create a ToolComplete event
162    pub fn tool_complete(call_id: &str, tool_name: &str, result: &str, success: bool, duration_ms: u64) -> Self {
163        AgentEvent::ToolComplete(ToolCompleteEvent {
164            call_id: call_id.to_string(),
165            tool_name: tool_name.to_string(),
166            result: result.to_string(),
167            success,
168            duration_ms,
169        })
170    }
171
172    /// Create a TurnEnd event
173    pub fn turn_end(content: &str, tool_call_count: usize, iterations: usize, usage: TokenUsageInfo) -> Self {
174        AgentEvent::TurnEnd(TurnEndEvent {
175            content: content.to_string(),
176            tool_call_count,
177            iterations,
178            usage,
179        })
180    }
181
182    /// Create a SessionEnd event
183    pub fn session_end(content: &str, total_iterations: usize, usage: TokenUsageInfo, reason: FinishReason) -> Self {
184        AgentEvent::SessionEnd(SessionEndEvent {
185            content: content.to_string(),
186            total_iterations,
187            usage,
188            finish_reason: reason,
189        })
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_turn_start_event() {
199        let event = AgentEvent::turn_start("Hello, world!");
200        assert!(matches!(event, AgentEvent::TurnStart(_)));
201    }
202
203    #[test]
204    fn test_thinking_delta_event() {
205        let event = AgentEvent::thinking_delta("Hello", true);
206        assert!(matches!(event, AgentEvent::ThinkingDelta(_)));
207    }
208
209    #[test]
210    fn test_tool_approval_event() {
211        let args = serde_json::json!({"command": "ls"});
212        let event = AgentEvent::tool_approval("call_1", "bash", args, true);
213        assert!(matches!(event, AgentEvent::ToolApproval(_)));
214    }
215
216    #[test]
217    fn test_tool_start_event() {
218        let args = serde_json::json!({"command": "ls"});
219        let event = AgentEvent::tool_start("call_1", "bash", args);
220        assert!(matches!(event, AgentEvent::ToolStart(_)));
221    }
222
223    #[test]
224    fn test_tool_complete_event() {
225        let event = AgentEvent::tool_complete("call_1", "bash", "file1\nfile2", true, 100);
226        assert!(matches!(event, AgentEvent::ToolComplete(_)));
227    }
228
229    #[test]
230    fn test_turn_end_event() {
231        let usage = TokenUsageInfo {
232            prompt_tokens: 100,
233            completion_tokens: 50,
234            total_tokens: 150,
235        };
236        let event = AgentEvent::turn_end("Done!", 2, 5, usage);
237        assert!(matches!(event, AgentEvent::TurnEnd(_)));
238    }
239
240    #[test]
241    fn test_session_end_event() {
242        let usage = TokenUsageInfo::default();
243        let event = AgentEvent::session_end("Goodbye!", 10, usage, FinishReason::Stop);
244        assert!(matches!(event, AgentEvent::SessionEnd(_)));
245    }
246
247    #[test]
248    fn test_finish_reason_variants() {
249        let stop = FinishReason::Stop;
250        let max_iter = FinishReason::MaxIterations;
251        let error = FinishReason::Error("something went wrong".to_string());
252        let unknown = FinishReason::UnknownTool("fake_tool".to_string());
253        let cancelled = FinishReason::Cancelled;
254
255        assert!(matches!(stop, FinishReason::Stop));
256        assert!(matches!(max_iter, FinishReason::MaxIterations));
257        assert!(matches!(error, FinishReason::Error(_)));
258        assert!(matches!(unknown, FinishReason::UnknownTool(_)));
259        assert!(matches!(cancelled, FinishReason::Cancelled));
260    }
261
262    #[test]
263    fn test_event_serialization() {
264        let event = AgentEvent::turn_start("test prompt");
265        let json = serde_json::to_string(&event).unwrap();
266        assert!(json.contains("TurnStart"));
267        assert!(json.contains("test prompt"));
268    }
269}