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(
144        call_id: &str,
145        tool_name: &str,
146        arguments: serde_json::Value,
147        auto_approved: bool,
148    ) -> Self {
149        AgentEvent::ToolApproval(ToolApprovalEvent {
150            call_id: call_id.to_string(),
151            tool_name: tool_name.to_string(),
152            arguments,
153            auto_approved,
154        })
155    }
156
157    /// Create a ToolStart event
158    pub fn tool_start(call_id: &str, tool_name: &str, arguments: serde_json::Value) -> Self {
159        AgentEvent::ToolStart(ToolStartEvent {
160            call_id: call_id.to_string(),
161            tool_name: tool_name.to_string(),
162            arguments,
163        })
164    }
165
166    /// Create a ToolComplete event
167    pub fn tool_complete(
168        call_id: &str,
169        tool_name: &str,
170        result: &str,
171        success: bool,
172        duration_ms: u64,
173    ) -> Self {
174        AgentEvent::ToolComplete(ToolCompleteEvent {
175            call_id: call_id.to_string(),
176            tool_name: tool_name.to_string(),
177            result: result.to_string(),
178            success,
179            duration_ms,
180        })
181    }
182
183    /// Create a TurnEnd event
184    pub fn turn_end(
185        content: &str,
186        tool_call_count: usize,
187        iterations: usize,
188        usage: TokenUsageInfo,
189    ) -> Self {
190        AgentEvent::TurnEnd(TurnEndEvent {
191            content: content.to_string(),
192            tool_call_count,
193            iterations,
194            usage,
195        })
196    }
197
198    /// Create a SessionEnd event
199    pub fn session_end(
200        content: &str,
201        total_iterations: usize,
202        usage: TokenUsageInfo,
203        reason: FinishReason,
204    ) -> Self {
205        AgentEvent::SessionEnd(SessionEndEvent {
206            content: content.to_string(),
207            total_iterations,
208            usage,
209            finish_reason: reason,
210        })
211    }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_turn_start_event() {
220        let event = AgentEvent::turn_start("Hello, world!");
221        assert!(matches!(event, AgentEvent::TurnStart(_)));
222    }
223
224    #[test]
225    fn test_thinking_delta_event() {
226        let event = AgentEvent::thinking_delta("Hello", true);
227        assert!(matches!(event, AgentEvent::ThinkingDelta(_)));
228    }
229
230    #[test]
231    fn test_tool_approval_event() {
232        let args = serde_json::json!({"command": "ls"});
233        let event = AgentEvent::tool_approval("call_1", "bash", args, true);
234        assert!(matches!(event, AgentEvent::ToolApproval(_)));
235    }
236
237    #[test]
238    fn test_tool_start_event() {
239        let args = serde_json::json!({"command": "ls"});
240        let event = AgentEvent::tool_start("call_1", "bash", args);
241        assert!(matches!(event, AgentEvent::ToolStart(_)));
242    }
243
244    #[test]
245    fn test_tool_complete_event() {
246        let event = AgentEvent::tool_complete("call_1", "bash", "file1\nfile2", true, 100);
247        assert!(matches!(event, AgentEvent::ToolComplete(_)));
248    }
249
250    #[test]
251    fn test_turn_end_event() {
252        let usage = TokenUsageInfo {
253            prompt_tokens: 100,
254            completion_tokens: 50,
255            total_tokens: 150,
256        };
257        let event = AgentEvent::turn_end("Done!", 2, 5, usage);
258        assert!(matches!(event, AgentEvent::TurnEnd(_)));
259    }
260
261    #[test]
262    fn test_session_end_event() {
263        let usage = TokenUsageInfo::default();
264        let event = AgentEvent::session_end("Goodbye!", 10, usage, FinishReason::Stop);
265        assert!(matches!(event, AgentEvent::SessionEnd(_)));
266    }
267
268    #[test]
269    fn test_finish_reason_variants() {
270        let stop = FinishReason::Stop;
271        let max_iter = FinishReason::MaxIterations;
272        let error = FinishReason::Error("something went wrong".to_string());
273        let unknown = FinishReason::UnknownTool("fake_tool".to_string());
274        let cancelled = FinishReason::Cancelled;
275
276        assert!(matches!(stop, FinishReason::Stop));
277        assert!(matches!(max_iter, FinishReason::MaxIterations));
278        assert!(matches!(error, FinishReason::Error(_)));
279        assert!(matches!(unknown, FinishReason::UnknownTool(_)));
280        assert!(matches!(cancelled, FinishReason::Cancelled));
281    }
282
283    #[test]
284    fn test_event_serialization() {
285        let event = AgentEvent::turn_start("test prompt");
286        let json = serde_json::to_string(&event).unwrap();
287        assert!(json.contains("TurnStart"));
288        assert!(json.contains("test prompt"));
289    }
290}