Skip to main content

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