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// ── Tool context types ────────────────────────────────────────────────────
8
9/// Semantic context for a tool execution event.
10///
11/// Carries structured information about *what* a tool call means,
12/// derived from the tool name and arguments by the agent loop.
13/// UI consumers that understand a context variant can render it
14/// richly; older consumers simply ignore the field.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "kind", rename_all = "snake_case")]
17#[non_exhaustive]
18pub enum ToolCallContext {
19 // ── Web exploration ──────────────────────────────────────
20 /// A search engine query.
21 WebSearch {
22 /// The search query string.
23 query: String,
24 /// Search engine used (e.g. "duckduckgo").
25 #[serde(skip_serializing_if = "Option::is_none")]
26 engine: Option<String>,
27 },
28
29 /// Visiting a web page.
30 PageVisit {
31 /// URL being visited.
32 url: String,
33 /// Why this page is being visited.
34 #[serde(skip_serializing_if = "Option::is_none")]
35 reason: Option<VisitReason>,
36 // ── Result fields (enriched by BrowseProgress::DocumentReady) ──
37 /// Page `<title>` after load.
38 #[serde(skip_serializing_if = "Option::is_none")]
39 page_title: Option<String>,
40 /// HTTP status code.
41 #[serde(skip_serializing_if = "Option::is_none")]
42 page_status: Option<u16>,
43 /// HTML body size in bytes.
44 #[serde(skip_serializing_if = "Option::is_none")]
45 page_bytes: Option<u64>,
46 /// Wall-clock page load duration in milliseconds.
47 #[serde(skip_serializing_if = "Option::is_none")]
48 page_duration_ms: Option<u64>,
49 },
50
51 /// Extracting data from a web page.
52 DataExtraction {
53 /// Description of what is being extracted (e.g. CSS selector).
54 target: String,
55 /// URL of the page being extracted from.
56 #[serde(skip_serializing_if = "Option::is_none")]
57 url: Option<String>,
58 // ── Result fields (enriched by BrowseProgress::DocumentReady) ──
59 /// Number of items extracted.
60 #[serde(skip_serializing_if = "Option::is_none")]
61 result_count: Option<usize>,
62 /// HTTP status code of the page.
63 #[serde(skip_serializing_if = "Option::is_none")]
64 page_status: Option<u16>,
65 /// Page load duration in milliseconds.
66 #[serde(skip_serializing_if = "Option::is_none")]
67 page_duration_ms: Option<u64>,
68 },
69
70 /// An action within a persistent browser session.
71 SessionAction {
72 /// The session action being performed (e.g. "goto", "click").
73 action: String,
74 /// URL if the action involves navigation.
75 #[serde(skip_serializing_if = "Option::is_none")]
76 url: Option<String>,
77 },
78
79 /// A step within a browse script.
80 ScriptStep {
81 /// Current step index (1-based).
82 current: usize,
83 /// Total number of steps.
84 total: usize,
85 /// Human-readable step description.
86 step: String,
87 },
88}
89
90/// Reason for visiting a page.
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub enum VisitReason {
94 /// The agent specified the URL directly.
95 DirectNavigation,
96 /// Clicked a search result at the given position.
97 SearchResult {
98 /// 1-based position in search results.
99 position: usize,
100 },
101 /// Followed a link from another page.
102 LinkFollowed {
103 /// The URL the link was on.
104 from_url: String,
105 },
106}
107
108/// Events emitted during agent execution.
109///
110/// Events are tagged with `type` and serialized as camelCase for JSON consumers.
111/// This enum is `#[non_exhaustive]` — new variants may be added in future releases.
112#[derive(Debug, Clone, Serialize, Deserialize)]
113#[serde(tag = "type", rename_all = "camelCase")]
114#[non_exhaustive]
115pub enum AgentEvent {
116 // ── Lifecycle events ──────────────────────────────────────────────
117 /// Emitted when the agent begins processing a batch of prompts.
118 AgentStart {
119 /// The initial prompt messages sent to the agent.
120 prompts: Vec<oxi_ai::Message>,
121 /// Optional session identifier for correlation.
122 session_id: Option<String>,
123 },
124
125 /// Emitted when the agent finishes all processing.
126 AgentEnd {
127 /// Final conversation messages.
128 messages: Vec<oxi_ai::Message>,
129 /// Why the agent stopped (e.g. `"end_turn"`, `"tool_use"`).
130 stop_reason: Option<String>,
131 /// Optional session identifier for correlation.
132 session_id: Option<String>,
133 },
134
135 /// Emitted at the start of each agent loop turn.
136 TurnStart {
137 /// Zero-based turn index.
138 turn_number: u32,
139 },
140
141 /// Emitted when a turn completes, including the assistant reply and tool results.
142 TurnEnd {
143 /// Turn index that just completed.
144 turn_number: u32,
145 /// The assistant message produced this turn.
146 assistant_message: oxi_ai::Message,
147 /// Tool results collected during this turn.
148 tool_results: Vec<oxi_ai::ToolResultMessage>,
149 },
150
151 // ── Message events ────────────────────────────────────────────────
152 /// A new message has been created in the conversation.
153 MessageStart {
154 /// The message that started.
155 message: oxi_ai::Message,
156 },
157
158 /// A message has been updated with new content.
159 MessageUpdate {
160 /// The message in its current state.
161 message: oxi_ai::Message,
162 /// Incremental text delta since the last update, if available.
163 delta: Option<String>,
164 },
165
166 /// A message has been finalized.
167 MessageEnd {
168 /// The completed message.
169 message: oxi_ai::Message,
170 },
171
172 // ── Tool execution events ────────────────────────────────────────
173 /// A tool is about to be executed.
174 ToolExecutionStart {
175 /// Unique identifier for this tool call.
176 tool_call_id: String,
177 /// Name of the tool being invoked.
178 tool_name: String,
179 /// JSON arguments passed to the tool.
180 args: serde_json::Value,
181 /// Semantic context inferred from tool name and arguments.
182 /// `None` for tools without a known context mapping.
183 #[serde(default, skip_serializing_if = "Option::is_none")]
184 context: Option<ToolCallContext>,
185 },
186
187 /// Partial progress from a running tool execution.
188 ToolExecutionUpdate {
189 /// Identifier of the tool call producing the update.
190 tool_call_id: String,
191 /// Name of the tool.
192 tool_name: String,
193 /// Partial result text so far.
194 partial_result: String,
195 /// Browser tab id that produced this progress (if the tool is
196 /// tab-aware). `None` for tools that don't have a tab concept,
197 /// or for older tool implementations that don't propagate tab ids.
198 #[serde(default, skip_serializing_if = "Option::is_none")]
199 tab_id: Option<uuid::Uuid>,
200 /// Semantic context inferred from tool name and arguments.
201 /// Carries structured information about what this update means.
202 #[serde(default, skip_serializing_if = "Option::is_none")]
203 context: Option<ToolCallContext>,
204 },
205
206 /// A tool execution has finished.
207 ToolExecutionEnd {
208 /// Identifier of the completed tool call.
209 tool_call_id: String,
210 /// Name of the tool.
211 tool_name: String,
212 /// The tool result payload.
213 result: oxi_ai::ToolResult,
214 /// Whether the tool execution resulted in an error.
215 is_error: bool,
216 },
217
218 // ── Legacy events (kept for backward compatibility) ──────────
219 /// Legacy: agent started processing a prompt.
220 #[serde(rename = "start")]
221 Start {
222 /// The user prompt that triggered the run.
223 prompt: String,
224 },
225
226 /// Agent is waiting for the first response token.
227 Thinking,
228
229 /// Incremental thinking / reasoning text from the model.
230 ThinkingDelta {
231 /// The reasoning text delta.
232 text: String,
233 },
234
235 /// A chunk of generated text from the model.
236 TextChunk {
237 /// The text delta to append.
238 text: String,
239 },
240
241 /// The model requested a tool call.
242 ToolCall {
243 /// The tool call descriptor from the provider.
244 tool_call: oxi_ai::ToolCall,
245 },
246
247 /// A tool execution has started.
248 ToolStart {
249 /// Identifier of the tool call.
250 tool_call_id: String,
251 /// Name of the tool being invoked.
252 tool_name: String,
253 /// JSON arguments for the tool call.
254 #[serde(default)]
255 arguments: serde_json::Value,
256 },
257
258 /// Progress update from a running tool.
259 ToolProgress {
260 /// Identifier of the tool call.
261 tool_call_id: String,
262 /// Human-readable progress message.
263 message: String,
264 },
265
266 /// A tool execution has completed.
267 ToolComplete {
268 /// The tool result payload.
269 result: oxi_ai::ToolResult,
270 },
271
272 /// A tool execution failed.
273 ToolError {
274 /// Identifier of the failed tool call.
275 tool_call_id: String,
276 /// Error description.
277 error: String,
278 },
279
280 /// The agent produced a final response.
281 Complete {
282 /// Full response text.
283 content: String,
284 /// Stop reason string (e.g. `"EndTurn"`).
285 stop_reason: String,
286 },
287
288 /// An error occurred during agent execution.
289 Error {
290 /// Human-readable error message.
291 message: String,
292 /// Optional session identifier.
293 session_id: Option<String>,
294 },
295
296 /// Agent loop iteration counter update.
297 Iteration {
298 /// Current iteration number.
299 number: usize,
300 },
301
302 /// Token usage report for a completed turn.
303 Usage {
304 /// Number of prompt / input tokens consumed.
305 input_tokens: usize,
306 /// Number of completion / output tokens produced.
307 output_tokens: usize,
308 },
309
310 /// Context compaction lifecycle event.
311 Compaction {
312 /// The underlying compaction event detail.
313 event: CompactionEvent,
314 },
315
316 /// The agent is retrying after a transient error.
317 Retry {
318 /// Current retry attempt (1-based).
319 attempt: usize,
320 /// Maximum number of retries allowed.
321 max_retries: usize,
322 /// Seconds until the next attempt.
323 retry_after_secs: u64,
324 /// Why the previous attempt failed.
325 reason: String,
326 /// Optional session identifier.
327 session_id: Option<String>,
328 },
329
330 /// The agent switched to a fallback model.
331 Fallback {
332 /// Model that was being used before the failure.
333 from_model: String,
334 /// Fallback model that will be used instead.
335 to_model: String,
336 },
337
338 /// The agent run was cancelled by the caller.
339 Cancelled,
340
341 /// A partial response delivered mid-stream (useful for UI rendering).
342 PartialResponse {
343 /// Accumulated response content so far.
344 content: String,
345 },
346
347 // ── Auto-retry events ─────────────────────────────────────────
348 /// An automatic retry attempt is starting.
349 AutoRetryStart {
350 /// Current retry attempt (1-based).
351 attempt: usize,
352 /// Total retry attempts that will be made.
353 max_attempts: usize,
354 /// Milliseconds before this attempt is sent.
355 delay_ms: u64,
356 /// The error that triggered the retry.
357 error_message: String,
358 },
359
360 /// An automatic retry attempt has concluded.
361 AutoRetryEnd {
362 /// Whether the retry succeeded.
363 success: bool,
364 /// Which attempt this was (1-based).
365 attempt: usize,
366 /// Final error if the retry failed, `None` on success.
367 final_error: Option<String>,
368 },
369
370 // ── Loop-specific steering events ─────────────────────────────
371 /// A system-level steering message injected into the conversation.
372 SteeringMessage {
373 /// The steering message to add to the context.
374 message: oxi_ai::Message,
375 },
376
377 /// A follow-up message appended to continue the conversation.
378 FollowUpMessage {
379 /// The follow-up message.
380 message: oxi_ai::Message,
381 },
382}
383
384impl AgentEvent {
385 /// Returns `true` if this event represents the end of the agent lifecycle.
386 pub fn is_terminal(&self) -> bool {
387 matches!(self, AgentEvent::AgentEnd { .. })
388 }
389
390 /// Returns the snake_case variant name of this event (useful for logging / serialization).
391 pub fn type_name(&self) -> &'static str {
392 match self {
393 AgentEvent::AgentStart { .. } => "agent_start",
394 AgentEvent::AgentEnd { .. } => "agent_end",
395 AgentEvent::TurnStart { .. } => "turn_start",
396 AgentEvent::TurnEnd { .. } => "turn_end",
397 AgentEvent::MessageStart { .. } => "message_start",
398 AgentEvent::MessageUpdate { .. } => "message_update",
399 AgentEvent::MessageEnd { .. } => "message_end",
400 AgentEvent::ToolExecutionStart { .. } => "tool_execution_start",
401 AgentEvent::ToolExecutionUpdate { .. } => "tool_execution_update",
402 AgentEvent::ToolExecutionEnd { .. } => "tool_execution_end",
403 AgentEvent::Start { .. } => "start",
404 AgentEvent::Thinking => "thinking",
405 AgentEvent::ThinkingDelta { .. } => "thinking_delta",
406 AgentEvent::TextChunk { .. } => "text_chunk",
407 AgentEvent::ToolCall { .. } => "tool_call",
408 AgentEvent::ToolStart { .. } => "tool_start",
409 AgentEvent::ToolProgress { .. } => "tool_progress",
410 AgentEvent::ToolComplete { .. } => "tool_complete",
411 AgentEvent::ToolError { .. } => "tool_error",
412 AgentEvent::Complete { .. } => "complete",
413 AgentEvent::Error { .. } => "error",
414 AgentEvent::Iteration { .. } => "iteration",
415 AgentEvent::Usage { .. } => "usage",
416 AgentEvent::Compaction { .. } => "compaction",
417 AgentEvent::Retry { .. } => "retry",
418 AgentEvent::Fallback { .. } => "fallback",
419 AgentEvent::Cancelled => "cancelled",
420 AgentEvent::PartialResponse { .. } => "partial_response",
421 AgentEvent::AutoRetryStart { .. } => "auto_retry_start",
422 AgentEvent::AutoRetryEnd { .. } => "auto_retry_end",
423 AgentEvent::SteeringMessage { .. } => "steering_message",
424 AgentEvent::FollowUpMessage { .. } => "follow_up_message",
425 }
426 }
427}