agent_sdk/
events.rs

1//! Agent events for real-time streaming.
2//!
3//! The [`AgentEvent`] enum represents all events that can occur during agent
4//! execution. These events are streamed via an async channel for real-time
5//! UI updates and logging.
6//!
7//! # Event Flow
8//!
9//! A typical event sequence looks like:
10//! 1. `Start` - Agent begins processing
11//! 2. `Text` / `ToolCallStart` / `ToolCallEnd` - Processing events
12//! 3. `TurnComplete` - One LLM round-trip finished
13//! 4. `Done` - Agent completed successfully, or `Error` if failed
14
15use crate::types::{ThreadId, TokenUsage, ToolResult, ToolTier};
16use serde::{Deserialize, Serialize};
17use std::time::Duration;
18
19/// Events emitted by the agent loop during execution.
20/// These are streamed to the client for real-time UI updates.
21#[derive(Clone, Debug, Serialize, Deserialize)]
22#[serde(tag = "type", rename_all = "snake_case")]
23pub enum AgentEvent {
24    /// Agent loop has started
25    Start { thread_id: ThreadId, turn: usize },
26
27    /// Agent is "thinking" - streaming text that may be shown as typing indicator
28    Thinking { text: String },
29
30    /// A text delta for streaming responses
31    TextDelta { delta: String },
32
33    /// Complete text block from the agent
34    Text { text: String },
35
36    /// Agent is about to call a tool
37    ToolCallStart {
38        id: String,
39        name: String,
40        input: serde_json::Value,
41        tier: ToolTier,
42    },
43
44    /// Tool execution completed
45    ToolCallEnd {
46        id: String,
47        name: String,
48        result: ToolResult,
49    },
50
51    /// Tool requires confirmation before execution
52    ToolRequiresConfirmation {
53        id: String,
54        name: String,
55        input: serde_json::Value,
56        description: String,
57    },
58
59    /// Tool requires PIN verification
60    ToolRequiresPin {
61        id: String,
62        name: String,
63        input: serde_json::Value,
64        description: String,
65    },
66
67    /// Agent turn completed (one LLM round-trip)
68    TurnComplete { turn: usize, usage: TokenUsage },
69
70    /// Agent loop completed successfully
71    Done {
72        thread_id: ThreadId,
73        total_turns: usize,
74        total_usage: TokenUsage,
75        duration: Duration,
76    },
77
78    /// An error occurred during execution
79    Error { message: String, recoverable: bool },
80
81    /// Context was compacted to reduce size
82    ContextCompacted {
83        /// Number of messages before compaction
84        original_count: usize,
85        /// Number of messages after compaction
86        new_count: usize,
87        /// Estimated tokens before compaction
88        original_tokens: usize,
89        /// Estimated tokens after compaction
90        new_tokens: usize,
91    },
92
93    /// Progress update from a running subagent
94    SubagentProgress {
95        /// ID of the parent tool call that spawned this subagent
96        subagent_id: String,
97        /// Name of the subagent (e.g., "explore", "plan")
98        subagent_name: String,
99        /// Tool name that just started or completed
100        tool_name: String,
101        /// Brief context for the tool (e.g., file path, pattern)
102        tool_context: String,
103        /// Whether the tool completed (false = started, true = ended)
104        completed: bool,
105        /// Whether the tool succeeded (only meaningful if completed)
106        success: bool,
107        /// Current total tool count for this subagent
108        tool_count: u32,
109        /// Current total tokens used by this subagent
110        total_tokens: u64,
111    },
112}
113
114impl AgentEvent {
115    #[must_use]
116    pub const fn start(thread_id: ThreadId, turn: usize) -> Self {
117        Self::Start { thread_id, turn }
118    }
119
120    #[must_use]
121    pub fn thinking(text: impl Into<String>) -> Self {
122        Self::Thinking { text: text.into() }
123    }
124
125    #[must_use]
126    pub fn text_delta(delta: impl Into<String>) -> Self {
127        Self::TextDelta {
128            delta: delta.into(),
129        }
130    }
131
132    #[must_use]
133    pub fn text(text: impl Into<String>) -> Self {
134        Self::Text { text: text.into() }
135    }
136
137    #[must_use]
138    pub fn tool_call_start(
139        id: impl Into<String>,
140        name: impl Into<String>,
141        input: serde_json::Value,
142        tier: ToolTier,
143    ) -> Self {
144        Self::ToolCallStart {
145            id: id.into(),
146            name: name.into(),
147            input,
148            tier,
149        }
150    }
151
152    #[must_use]
153    pub fn tool_call_end(
154        id: impl Into<String>,
155        name: impl Into<String>,
156        result: ToolResult,
157    ) -> Self {
158        Self::ToolCallEnd {
159            id: id.into(),
160            name: name.into(),
161            result,
162        }
163    }
164
165    #[must_use]
166    pub const fn done(
167        thread_id: ThreadId,
168        total_turns: usize,
169        total_usage: TokenUsage,
170        duration: Duration,
171    ) -> Self {
172        Self::Done {
173            thread_id,
174            total_turns,
175            total_usage,
176            duration,
177        }
178    }
179
180    #[must_use]
181    pub fn error(message: impl Into<String>, recoverable: bool) -> Self {
182        Self::Error {
183            message: message.into(),
184            recoverable,
185        }
186    }
187
188    #[must_use]
189    pub const fn context_compacted(
190        original_count: usize,
191        new_count: usize,
192        original_tokens: usize,
193        new_tokens: usize,
194    ) -> Self {
195        Self::ContextCompacted {
196            original_count,
197            new_count,
198            original_tokens,
199            new_tokens,
200        }
201    }
202}