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::execution::ContextBudget;
6use crate::hooks::InlineHookRequest;
7
8/// Token usage information for a run
9#[derive(Debug, Serialize, Deserialize, Clone, Default)]
10pub struct RunUsage {
11    /// Actual tokens used (from LLM response)
12    pub total_tokens: u32,
13    pub input_tokens: u32,
14    pub output_tokens: u32,
15    /// Tokens read from provider cache (e.g., Anthropic prompt caching)
16    #[serde(default)]
17    pub cached_tokens: u32,
18    /// Estimated tokens (pre-call estimate)
19    pub estimated_tokens: u32,
20    /// Model used for this run (e.g., "gpt-5.1", "claude-sonnet-4")
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub model: Option<String>,
23    /// Estimated cost in USD (based on model pricing)
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub cost_usd: Option<f64>,
26}
27
28#[derive(Debug, Serialize, Deserialize, Clone)]
29#[serde(rename_all = "snake_case")]
30pub struct AgentEvent {
31    pub timestamp: chrono::DateTime<chrono::Utc>,
32    pub thread_id: String,
33    pub run_id: String,
34    pub event: AgentEventType,
35    pub task_id: String,
36    pub agent_id: String,
37    /// User ID for usage tracking
38    #[serde(default)]
39    pub user_id: Option<String>,
40    /// Identifier ID for tenant/project-level usage tracking
41    #[serde(default)]
42    pub identifier_id: Option<String>,
43    /// Workspace ID for workspace-scoped usage tracking
44    #[serde(default)]
45    pub workspace_id: Option<String>,
46    /// Channel ID for channel-scoped usage tracking
47    #[serde(default)]
48    pub channel_id: Option<String>,
49}
50
51impl AgentEvent {
52    /// Reconstruct an AgentEvent from a stored TaskEvent (e.g. for history replay).
53    pub fn from_task_event(task_event: &crate::TaskEvent, thread_id: &str) -> Self {
54        Self {
55            event: task_event.event.clone(),
56            agent_id: String::new(),
57            timestamp: chrono::DateTime::from_timestamp_millis(task_event.created_at)
58                .unwrap_or_default(),
59            thread_id: thread_id.to_string(),
60            run_id: String::new(),
61            task_id: String::new(),
62            user_id: None,
63            identifier_id: None,
64            workspace_id: None,
65            channel_id: None,
66        }
67    }
68}
69
70#[derive(Debug, Serialize, Deserialize, Clone)]
71#[serde(rename_all = "snake_case", tag = "type")]
72#[allow(clippy::large_enum_variant)]
73pub enum AgentEventType {
74    /// Verbose diagnostic message streamed from server to client (only emitted when verbose=true).
75    DiagnosticLog {
76        message: String,
77    },
78
79    // Main run events
80    RunStarted {},
81    RunFinished {
82        success: bool,
83        total_steps: usize,
84        failed_steps: usize,
85        /// Token usage for this run
86        usage: Option<RunUsage>,
87        #[serde(default, skip_serializing_if = "Option::is_none")]
88        context_budget: Option<ContextBudget>,
89    },
90    RunError {
91        message: String,
92        code: Option<String>,
93        /// Cumulative token usage at the point of failure
94        #[serde(default, skip_serializing_if = "Option::is_none")]
95        usage: Option<RunUsage>,
96    },
97    PlanStarted {
98        initial_plan: bool,
99    },
100    PlanFinished {
101        total_steps: usize,
102    },
103    PlanPruned {
104        removed_steps: Vec<String>,
105    },
106    // Step execution events
107    StepStarted {
108        step_id: String,
109        step_index: usize,
110    },
111    StepCompleted {
112        step_id: String,
113        success: bool,
114        #[serde(default, skip_serializing_if = "Option::is_none")]
115        context_budget: Option<ContextBudget>,
116        /// Cumulative token usage for this run up to this step
117        #[serde(default, skip_serializing_if = "Option::is_none")]
118        usage: Option<RunUsage>,
119    },
120
121    // Reflection events (emitted when is_reflection_enabled() and reflection runs)
122    ReflectStarted {},
123    ReflectFinished {
124        should_retry: bool,
125        #[serde(default, skip_serializing_if = "Option::is_none")]
126        reason: Option<String>,
127    },
128
129    // Tool execution events
130    ToolExecutionStart {
131        step_id: String,
132        tool_call_id: String,
133        tool_call_name: String,
134        input: Value,
135    },
136    ToolExecutionEnd {
137        step_id: String,
138        tool_call_id: String,
139        tool_call_name: String,
140        success: bool,
141    },
142
143    // Message events for streaming
144    TextMessageStart {
145        message_id: String,
146        step_id: String,
147        role: MessageRole,
148        is_final: Option<bool>,
149    },
150    TextMessageContent {
151        message_id: String,
152        step_id: String,
153        delta: String,
154        stripped_content: Option<Vec<(usize, String)>>,
155    },
156    TextMessageEnd {
157        message_id: String,
158        step_id: String,
159    },
160
161    // Tool call events with parent/child relationships
162    ToolCalls {
163        step_id: String,
164        parent_message_id: Option<String>,
165        tool_calls: Vec<ToolCall>,
166    },
167    ToolResults {
168        step_id: String,
169        parent_message_id: Option<String>,
170        results: Vec<ToolResponse>,
171    },
172
173    // Agent transfer events
174    AgentHandover {
175        from_agent: String,
176        to_agent: String,
177        reason: Option<String>,
178    },
179
180    /// A live, embeddable view produced by a tool (e.g. browsr viewer, Grafana
181    /// dashboard, map widget). The channel renders it inline as an iframe
182    /// (web) or as a clickable link (Telegram, WhatsApp, CLI).
183    LiveView {
184        /// Unique ID for this view — used for updates and teardown
185        view_id: String,
186        /// URL to embed or link (must be https:// for iframe security)
187        url: String,
188        /// Human-readable title for the view
189        #[serde(default, skip_serializing_if = "Option::is_none")]
190        title: Option<String>,
191        /// Display mode hint: "inline", "fullscreen", or "pip"
192        #[serde(default, skip_serializing_if = "Option::is_none")]
193        display_mode: Option<String>,
194        /// Width hint in pixels
195        #[serde(default, skip_serializing_if = "Option::is_none")]
196        width: Option<u32>,
197        /// Height hint in pixels
198        #[serde(default, skip_serializing_if = "Option::is_none")]
199        height: Option<u32>,
200    },
201
202    BrowserSessionStarted {
203        session_id: String,
204        viewer_url: Option<String>,
205        stream_url: Option<String>,
206    },
207
208    InlineHookRequested {
209        request: InlineHookRequest,
210    },
211
212    // TODO events
213    TodosUpdated {
214        formatted_todos: String,
215        action: String,
216        todo_count: usize,
217    },
218
219    // Context management events
220    ContextCompaction {
221        tier: CompactionTier,
222        tokens_before: usize,
223        tokens_after: usize,
224        entries_affected: usize,
225        context_limit: usize,
226        usage_ratio: f64,
227        summary: Option<String>,
228        /// Skill IDs re-injected after compaction
229        #[serde(default, skip_serializing_if = "Vec::is_empty")]
230        reinjected_skills: Vec<String>,
231        #[serde(default, skip_serializing_if = "Option::is_none")]
232        context_budget: Option<ContextBudget>,
233    },
234
235    /// Emitted each turn with the current context budget breakdown.
236    ContextBudgetUpdate {
237        budget: ContextBudget,
238        is_warning: bool,
239        is_critical: bool,
240    },
241}
242
243/// Tier of context compaction applied
244#[derive(Debug, Clone, Serialize, Deserialize)]
245#[serde(rename_all = "snake_case")]
246pub enum CompactionTier {
247    /// Mechanical: drop old entries, truncate payloads
248    Trim,
249    /// Semantic: LLM-powered summarization of history
250    Summarize,
251    /// Emergency: preserve only essentials
252    Reset,
253}
254
255impl AgentEvent {
256    pub fn new(event: AgentEventType) -> Self {
257        Self {
258            timestamp: chrono::Utc::now(),
259            thread_id: uuid::Uuid::new_v4().to_string(),
260            run_id: uuid::Uuid::new_v4().to_string(),
261            event,
262            task_id: uuid::Uuid::new_v4().to_string(),
263            agent_id: "default".to_string(),
264            user_id: None,
265            identifier_id: None,
266            workspace_id: None,
267            channel_id: None,
268        }
269    }
270
271    pub fn with_context(
272        event: AgentEventType,
273        thread_id: String,
274        run_id: String,
275        task_id: String,
276        agent_id: String,
277    ) -> Self {
278        Self {
279            timestamp: chrono::Utc::now(),
280            thread_id,
281            run_id,
282            task_id,
283            event,
284            agent_id,
285            user_id: None,
286            identifier_id: None,
287            workspace_id: None,
288            channel_id: None,
289        }
290    }
291}