oxi_agent/events.rs
1/// Agent event system
2/// Defines all events emitted during an agent run, including lifecycle,
3/// streaming, tool execution, compaction, retry, and steering events.
4use crate::compaction::CompactionEvent;
5use serde::{Deserialize, Serialize};
6
7/// Events emitted during agent execution.
8///
9/// Events are tagged with `type` and serialized as camelCase for JSON consumers.
10/// This enum is `#[non_exhaustive]` — new variants may be added in future releases.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(tag = "type", rename_all = "camelCase")]
13#[non_exhaustive]
14pub enum AgentEvent {
15 // ── Lifecycle events ──────────────────────────────────────────────
16 /// Emitted when the agent begins processing a batch of prompts.
17 AgentStart {
18 /// The initial prompt messages sent to the agent.
19 prompts: Vec<oxi_ai::Message>,
20 /// Optional session identifier for correlation.
21 session_id: Option<String>,
22 },
23
24 /// Emitted when the agent finishes all processing.
25 AgentEnd {
26 /// Final conversation messages.
27 messages: Vec<oxi_ai::Message>,
28 /// Why the agent stopped (e.g. `"end_turn"`, `"tool_use"`).
29 stop_reason: Option<String>,
30 /// Optional session identifier for correlation.
31 session_id: Option<String>,
32 },
33
34 /// Emitted at the start of each agent loop turn.
35 TurnStart {
36 /// Zero-based turn index.
37 turn_number: u32,
38 },
39
40 /// Emitted when a turn completes, including the assistant reply and tool results.
41 TurnEnd {
42 /// Turn index that just completed.
43 turn_number: u32,
44 /// The assistant message produced this turn.
45 assistant_message: oxi_ai::Message,
46 /// Tool results collected during this turn.
47 tool_results: Vec<oxi_ai::ToolResultMessage>,
48 },
49
50 // ── Message events ────────────────────────────────────────────────
51 /// A new message has been created in the conversation.
52 MessageStart {
53 /// The message that started.
54 message: oxi_ai::Message,
55 },
56
57 /// A message has been updated with new content.
58 MessageUpdate {
59 /// The message in its current state.
60 message: oxi_ai::Message,
61 /// Incremental text delta since the last update, if available.
62 delta: Option<String>,
63 },
64
65 /// A message has been finalized.
66 MessageEnd {
67 /// The completed message.
68 message: oxi_ai::Message,
69 },
70
71 // ── Tool execution events ────────────────────────────────────────
72 /// A tool is about to be executed.
73 ToolExecutionStart {
74 /// Unique identifier for this tool call.
75 tool_call_id: String,
76 /// Name of the tool being invoked.
77 tool_name: String,
78 /// JSON arguments passed to the tool.
79 args: serde_json::Value,
80 },
81
82 /// Partial progress from a running tool execution.
83 ToolExecutionUpdate {
84 /// Identifier of the tool call producing the update.
85 tool_call_id: String,
86 /// Name of the tool.
87 tool_name: String,
88 /// Partial result text so far.
89 partial_result: String,
90 /// Browser tab id that produced this progress (if the tool is
91 /// tab-aware). `None` for tools that don't have a tab concept,
92 /// or for older tool implementations that don't propagate tab ids.
93 #[serde(default, skip_serializing_if = "Option::is_none")]
94 tab_id: Option<uuid::Uuid>,
95 },
96
97 /// A tool execution has finished.
98 ToolExecutionEnd {
99 /// Identifier of the completed tool call.
100 tool_call_id: String,
101 /// Name of the tool.
102 tool_name: String,
103 /// The tool result payload.
104 result: oxi_ai::ToolResult,
105 /// Whether the tool execution resulted in an error.
106 is_error: bool,
107 },
108
109 // ── Legacy events (kept for backward compatibility) ──────────
110 /// Legacy: agent started processing a prompt.
111 #[serde(rename = "start")]
112 Start {
113 /// The user prompt that triggered the run.
114 prompt: String,
115 },
116
117 /// Agent is waiting for the first response token.
118 Thinking,
119
120 /// Incremental thinking / reasoning text from the model.
121 ThinkingDelta {
122 /// The reasoning text delta.
123 text: String,
124 },
125
126 /// A chunk of generated text from the model.
127 TextChunk {
128 /// The text delta to append.
129 text: String,
130 },
131
132 /// The model requested a tool call.
133 ToolCall {
134 /// The tool call descriptor from the provider.
135 tool_call: oxi_ai::ToolCall,
136 },
137
138 /// A tool execution has started.
139 ToolStart {
140 /// Identifier of the tool call.
141 tool_call_id: String,
142 /// Name of the tool being invoked.
143 tool_name: String,
144 /// JSON arguments for the tool call.
145 #[serde(default)]
146 arguments: serde_json::Value,
147 },
148
149 /// Progress update from a running tool.
150 ToolProgress {
151 /// Identifier of the tool call.
152 tool_call_id: String,
153 /// Human-readable progress message.
154 message: String,
155 },
156
157 /// A tool execution has completed.
158 ToolComplete {
159 /// The tool result payload.
160 result: oxi_ai::ToolResult,
161 },
162
163 /// A tool execution failed.
164 ToolError {
165 /// Identifier of the failed tool call.
166 tool_call_id: String,
167 /// Error description.
168 error: String,
169 },
170
171 /// The agent produced a final response.
172 Complete {
173 /// Full response text.
174 content: String,
175 /// Stop reason string (e.g. `"EndTurn"`).
176 stop_reason: String,
177 },
178
179 /// An error occurred during agent execution.
180 Error {
181 /// Human-readable error message.
182 message: String,
183 /// Optional session identifier.
184 session_id: Option<String>,
185 },
186
187 /// Agent loop iteration counter update.
188 Iteration {
189 /// Current iteration number.
190 number: usize,
191 },
192
193 /// Token usage report for a completed turn.
194 Usage {
195 /// Number of prompt / input tokens consumed.
196 input_tokens: usize,
197 /// Number of completion / output tokens produced.
198 output_tokens: usize,
199 },
200
201 /// Context compaction lifecycle event.
202 Compaction {
203 /// The underlying compaction event detail.
204 event: CompactionEvent,
205 },
206
207 /// The agent is retrying after a transient error.
208 Retry {
209 /// Current retry attempt (1-based).
210 attempt: usize,
211 /// Maximum number of retries allowed.
212 max_retries: usize,
213 /// Seconds until the next attempt.
214 retry_after_secs: u64,
215 /// Why the previous attempt failed.
216 reason: String,
217 /// Optional session identifier.
218 session_id: Option<String>,
219 },
220
221 /// The agent switched to a fallback model.
222 Fallback {
223 /// Model that was being used before the failure.
224 from_model: String,
225 /// Fallback model that will be used instead.
226 to_model: String,
227 },
228
229 /// The agent run was cancelled by the caller.
230 Cancelled,
231
232 /// A partial response delivered mid-stream (useful for UI rendering).
233 PartialResponse {
234 /// Accumulated response content so far.
235 content: String,
236 },
237
238 // ── Auto-retry events ─────────────────────────────────────────
239 /// An automatic retry attempt is starting.
240 AutoRetryStart {
241 /// Current retry attempt (1-based).
242 attempt: usize,
243 /// Total retry attempts that will be made.
244 max_attempts: usize,
245 /// Milliseconds before this attempt is sent.
246 delay_ms: u64,
247 /// The error that triggered the retry.
248 error_message: String,
249 },
250
251 /// An automatic retry attempt has concluded.
252 AutoRetryEnd {
253 /// Whether the retry succeeded.
254 success: bool,
255 /// Which attempt this was (1-based).
256 attempt: usize,
257 /// Final error if the retry failed, `None` on success.
258 final_error: Option<String>,
259 },
260
261 // ── Loop-specific steering events ─────────────────────────────
262 /// A system-level steering message injected into the conversation.
263 SteeringMessage {
264 /// The steering message to add to the context.
265 message: oxi_ai::Message,
266 },
267
268 /// A follow-up message appended to continue the conversation.
269 FollowUpMessage {
270 /// The follow-up message.
271 message: oxi_ai::Message,
272 },
273}
274
275impl AgentEvent {
276 /// Returns `true` if this event represents the end of the agent lifecycle.
277 pub fn is_terminal(&self) -> bool {
278 matches!(self, AgentEvent::AgentEnd { .. })
279 }
280
281 /// Returns the snake_case variant name of this event (useful for logging / serialization).
282 pub fn type_name(&self) -> &'static str {
283 match self {
284 AgentEvent::AgentStart { .. } => "agent_start",
285 AgentEvent::AgentEnd { .. } => "agent_end",
286 AgentEvent::TurnStart { .. } => "turn_start",
287 AgentEvent::TurnEnd { .. } => "turn_end",
288 AgentEvent::MessageStart { .. } => "message_start",
289 AgentEvent::MessageUpdate { .. } => "message_update",
290 AgentEvent::MessageEnd { .. } => "message_end",
291 AgentEvent::ToolExecutionStart { .. } => "tool_execution_start",
292 AgentEvent::ToolExecutionUpdate { .. } => "tool_execution_update",
293 AgentEvent::ToolExecutionEnd { .. } => "tool_execution_end",
294 AgentEvent::Start { .. } => "start",
295 AgentEvent::Thinking => "thinking",
296 AgentEvent::ThinkingDelta { .. } => "thinking_delta",
297 AgentEvent::TextChunk { .. } => "text_chunk",
298 AgentEvent::ToolCall { .. } => "tool_call",
299 AgentEvent::ToolStart { .. } => "tool_start",
300 AgentEvent::ToolProgress { .. } => "tool_progress",
301 AgentEvent::ToolComplete { .. } => "tool_complete",
302 AgentEvent::ToolError { .. } => "tool_error",
303 AgentEvent::Complete { .. } => "complete",
304 AgentEvent::Error { .. } => "error",
305 AgentEvent::Iteration { .. } => "iteration",
306 AgentEvent::Usage { .. } => "usage",
307 AgentEvent::Compaction { .. } => "compaction",
308 AgentEvent::Retry { .. } => "retry",
309 AgentEvent::Fallback { .. } => "fallback",
310 AgentEvent::Cancelled => "cancelled",
311 AgentEvent::PartialResponse { .. } => "partial_response",
312 AgentEvent::AutoRetryStart { .. } => "auto_retry_start",
313 AgentEvent::AutoRetryEnd { .. } => "auto_retry_end",
314 AgentEvent::SteeringMessage { .. } => "steering_message",
315 AgentEvent::FollowUpMessage { .. } => "follow_up_message",
316 }
317 }
318}