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    },
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}