Skip to main content

distri_types/
events.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::core::{MessageRole, ToolCall, ToolResponse};
5use crate::hooks::InlineHookRequest;
6
7/// Token usage information for a run
8#[derive(Debug, Serialize, Deserialize, Clone, Default)]
9pub struct RunUsage {
10    /// Actual tokens used (from LLM response)
11    pub total_tokens: u32,
12    pub input_tokens: u32,
13    pub output_tokens: u32,
14    /// Tokens read from provider cache (e.g., Anthropic prompt caching)
15    #[serde(default)]
16    pub cached_tokens: u32,
17    /// Estimated tokens (pre-call estimate)
18    pub estimated_tokens: u32,
19    /// Model used for this run (e.g., "gpt-5.1", "claude-sonnet-4")
20    #[serde(default, skip_serializing_if = "Option::is_none")]
21    pub model: Option<String>,
22    /// Estimated cost in USD (based on model pricing)
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub cost_usd: Option<f64>,
25}
26
27#[derive(Debug, Serialize, Deserialize, Clone)]
28#[serde(rename_all = "snake_case")]
29pub struct AgentEvent {
30    pub timestamp: chrono::DateTime<chrono::Utc>,
31    pub thread_id: String,
32    pub run_id: String,
33    pub event: AgentEventType,
34    pub task_id: String,
35    pub agent_id: String,
36    /// User ID for usage tracking
37    #[serde(default)]
38    pub user_id: Option<String>,
39    /// Identifier ID for tenant/project-level usage tracking
40    #[serde(default)]
41    pub identifier_id: Option<String>,
42    /// Workspace ID for workspace-scoped usage tracking
43    #[serde(default)]
44    pub workspace_id: Option<String>,
45    /// Channel ID for channel-scoped usage tracking
46    #[serde(default)]
47    pub channel_id: Option<String>,
48}
49
50impl AgentEvent {
51    /// Reconstruct an AgentEvent from a stored TaskEvent (e.g. for history replay).
52    pub fn from_task_event(task_event: &crate::TaskEvent, thread_id: &str) -> Self {
53        Self {
54            event: task_event.event.clone(),
55            agent_id: String::new(),
56            timestamp: chrono::DateTime::from_timestamp_millis(task_event.created_at)
57                .unwrap_or_default(),
58            thread_id: thread_id.to_string(),
59            run_id: String::new(),
60            task_id: String::new(),
61            user_id: None,
62            identifier_id: None,
63            workspace_id: None,
64            channel_id: None,
65        }
66    }
67}
68
69#[derive(Debug, Serialize, Deserialize, Clone)]
70#[serde(rename_all = "snake_case", tag = "type")]
71pub enum AgentEventType {
72    // Main run events
73    RunStarted {},
74    RunFinished {
75        success: bool,
76        total_steps: usize,
77        failed_steps: usize,
78        /// Token usage for this run
79        usage: Option<RunUsage>,
80    },
81    RunError {
82        message: String,
83        code: Option<String>,
84    },
85    PlanStarted {
86        initial_plan: bool,
87    },
88    PlanFinished {
89        total_steps: usize,
90    },
91    PlanPruned {
92        removed_steps: Vec<String>,
93    },
94    // Step execution events
95    StepStarted {
96        step_id: String,
97        step_index: usize,
98    },
99    StepCompleted {
100        step_id: String,
101        success: bool,
102    },
103
104    // Tool execution events
105    ToolExecutionStart {
106        step_id: String,
107        tool_call_id: String,
108        tool_call_name: String,
109        input: Value,
110    },
111    ToolExecutionEnd {
112        step_id: String,
113        tool_call_id: String,
114        tool_call_name: String,
115        success: bool,
116    },
117
118    // Message events for streaming
119    TextMessageStart {
120        message_id: String,
121        step_id: String,
122        role: MessageRole,
123        is_final: Option<bool>,
124    },
125    TextMessageContent {
126        message_id: String,
127        step_id: String,
128        delta: String,
129        stripped_content: Option<Vec<(usize, String)>>,
130    },
131    TextMessageEnd {
132        message_id: String,
133        step_id: String,
134    },
135
136    // Tool call events with parent/child relationships
137    ToolCalls {
138        step_id: String,
139        parent_message_id: Option<String>,
140        tool_calls: Vec<ToolCall>,
141    },
142    ToolResults {
143        step_id: String,
144        parent_message_id: Option<String>,
145        results: Vec<ToolResponse>,
146    },
147
148    // Agent transfer events
149    AgentHandover {
150        from_agent: String,
151        to_agent: String,
152        reason: Option<String>,
153    },
154
155    BrowserScreenshot {
156        image: String,
157        format: Option<String>,
158        filename: Option<String>,
159        size: Option<u64>,
160        timestamp_ms: Option<i64>,
161    },
162
163    BrowserSessionStarted {
164        session_id: String,
165        viewer_url: Option<String>,
166        stream_url: Option<String>,
167    },
168
169    InlineHookRequested {
170        request: InlineHookRequest,
171    },
172
173    // TODO events
174    TodosUpdated {
175        formatted_todos: String,
176        action: String,
177        todo_count: usize,
178    },
179
180    // Context management events
181    ContextCompaction {
182        /// Which tier of compaction was applied
183        tier: CompactionTier,
184        /// Token count before compaction
185        tokens_before: usize,
186        /// Token count after compaction
187        tokens_after: usize,
188        /// Number of entries removed or summarized
189        entries_affected: usize,
190        /// Context budget limit that triggered compaction
191        context_limit: usize,
192        /// Usage ratio that triggered compaction (0.0 - 1.0)
193        usage_ratio: f64,
194        /// Optional summary text (for Tier 2 summarization)
195        summary: Option<String>,
196    },
197}
198
199/// Tier of context compaction applied
200#[derive(Debug, Clone, Serialize, Deserialize)]
201#[serde(rename_all = "snake_case")]
202pub enum CompactionTier {
203    /// Mechanical: drop old entries, truncate payloads
204    Trim,
205    /// Semantic: LLM-powered summarization of history
206    Summarize,
207    /// Emergency: preserve only essentials
208    Reset,
209}
210
211impl AgentEvent {
212    pub fn new(event: AgentEventType) -> Self {
213        Self {
214            timestamp: chrono::Utc::now(),
215            thread_id: uuid::Uuid::new_v4().to_string(),
216            run_id: uuid::Uuid::new_v4().to_string(),
217            event,
218            task_id: uuid::Uuid::new_v4().to_string(),
219            agent_id: "default".to_string(),
220            user_id: None,
221            identifier_id: None,
222            workspace_id: None,
223            channel_id: None,
224        }
225    }
226
227    pub fn with_context(
228        event: AgentEventType,
229        thread_id: String,
230        run_id: String,
231        task_id: String,
232        agent_id: String,
233    ) -> Self {
234        Self {
235            timestamp: chrono::Utc::now(),
236            thread_id,
237            run_id,
238            task_id,
239            event,
240            agent_id,
241            user_id: None,
242            identifier_id: None,
243            workspace_id: None,
244            channel_id: None,
245        }
246    }
247}