Skip to main content

cersei_agent/
events.rs

1//! Agent events: the full event enum, AgentStream, and control messages.
2
3use cersei_tools::permissions::{PermissionDecision, PermissionRequest};
4use cersei_tools::PermissionLevel;
5use cersei_types::*;
6use std::time::Duration;
7use tokio::sync::mpsc;
8
9use crate::AgentOutput;
10
11// ─── Agent events ────────────────────────────────────────────────────────────
12
13#[derive(Debug, Clone)]
14pub enum AgentEvent {
15    // Streaming content
16    TextDelta(String),
17    ThinkingDelta(String),
18
19    // Tool lifecycle
20    ToolStart {
21        name: String,
22        id: String,
23        input: serde_json::Value,
24    },
25    ToolEnd {
26        name: String,
27        id: String,
28        result: String,
29        is_error: bool,
30        duration: Duration,
31    },
32    ToolPermissionCheck {
33        name: String,
34        id: String,
35        level: PermissionLevel,
36    },
37
38    // Permission interaction
39    PermissionRequired(PermissionRequest),
40
41    // Turn lifecycle
42    TurnStart {
43        turn: u32,
44    },
45    TurnComplete {
46        turn: u32,
47        stop_reason: StopReason,
48        usage: Usage,
49    },
50    ModelRequestStart {
51        turn: u32,
52        message_count: usize,
53        token_estimate: u64,
54    },
55    ModelResponseStart {
56        turn: u32,
57        model: String,
58    },
59
60    // Context management
61    TokenWarning {
62        pct_used: f64,
63        state: WarningState,
64    },
65    CompactStart {
66        reason: CompactReason,
67        messages_before: usize,
68    },
69    CompactEnd {
70        messages_after: usize,
71        tokens_freed: u64,
72    },
73
74    // Session lifecycle
75    SessionLoaded {
76        session_id: String,
77        message_count: usize,
78    },
79    SessionSaved {
80        session_id: String,
81    },
82
83    // Cost tracking (realtime)
84    CostUpdate {
85        turn_cost: f64,
86        cumulative_cost: f64,
87        input_tokens: u64,
88        output_tokens: u64,
89    },
90
91    // Agent coordination (multi-agent)
92    SubAgentSpawned {
93        agent_id: String,
94        prompt: String,
95    },
96    SubAgentComplete {
97        agent_id: String,
98        result: AgentOutput,
99    },
100
101    // Hook activity
102    HookFired {
103        event: cersei_hooks::HookEvent,
104        hook_name: String,
105    },
106    HookBlocked {
107        event: cersei_hooks::HookEvent,
108        hook_name: String,
109        reason: String,
110    },
111
112    // Terminal
113    Status(String),
114    Error(String),
115    Complete(AgentOutput),
116}
117
118#[derive(Debug, Clone, Copy)]
119pub enum WarningState {
120    Normal,
121    Warning,
122    Critical,
123}
124
125#[derive(Debug, Clone, Copy)]
126pub enum CompactReason {
127    ThresholdExceeded,
128    ManualTrigger,
129    ContextOverflow,
130}
131
132// ─── Agent stream ────────────────────────────────────────────────────────────
133
134/// Returned by `agent.run_stream()`. Provides async iteration over events
135/// and bidirectional control (permissions, cancellation, message injection).
136pub struct AgentStream {
137    rx: mpsc::Receiver<AgentEvent>,
138    control_tx: mpsc::Sender<AgentControl>,
139}
140
141impl AgentStream {
142    pub(crate) fn new(
143        rx: mpsc::Receiver<AgentEvent>,
144        control_tx: mpsc::Sender<AgentControl>,
145    ) -> Self {
146        Self { rx, control_tx }
147    }
148
149    /// Respond to a PermissionRequired event.
150    pub fn respond_permission(&self, request_id: String, decision: PermissionDecision) {
151        let _ = self.control_tx.try_send(AgentControl::PermissionResponse {
152            request_id,
153            decision,
154        });
155    }
156
157    /// Send a cancellation signal.
158    pub fn cancel(&self) {
159        let _ = self.control_tx.try_send(AgentControl::Cancel);
160    }
161
162    /// Inject a user message mid-stream.
163    pub fn inject_message(&self, message: String) {
164        let _ = self
165            .control_tx
166            .try_send(AgentControl::InjectMessage(message));
167    }
168
169    /// Receive the next event.
170    pub async fn next(&mut self) -> Option<AgentEvent> {
171        self.rx.recv().await
172    }
173
174    /// Collect all events and return the final output.
175    pub async fn collect(mut self) -> cersei_types::Result<AgentOutput> {
176        while let Some(event) = self.rx.recv().await {
177            match event {
178                AgentEvent::Complete(output) => return Ok(output),
179                AgentEvent::Error(e) => return Err(CerseiError::Other(anyhow::anyhow!(e))),
180                _ => continue,
181            }
182        }
183        Err(CerseiError::Cancelled)
184    }
185
186    /// Collect only text deltas into a single string.
187    pub async fn collect_text(mut self) -> cersei_types::Result<String> {
188        let mut text = String::new();
189        while let Some(event) = self.rx.recv().await {
190            match event {
191                AgentEvent::TextDelta(t) => text.push_str(&t),
192                AgentEvent::Complete(_) => return Ok(text),
193                AgentEvent::Error(e) => return Err(CerseiError::Other(anyhow::anyhow!(e))),
194                _ => continue,
195            }
196        }
197        Ok(text)
198    }
199}
200
201// ─── Control messages ────────────────────────────────────────────────────────
202
203#[derive(Debug)]
204pub(crate) enum AgentControl {
205    #[allow(dead_code)]
206    PermissionResponse {
207        request_id: String,
208        decision: PermissionDecision,
209    },
210    Cancel,
211    #[allow(dead_code)]
212    InjectMessage(String),
213}