rpi_agent/events.rs
1//! Mirrors `packages/agent/src/types.ts::AgentEvent` — the lifecycle events the
2//! agent loop emits. Carried over a `broadcast::Sender<AgentEvent>` so multiple
3//! subscribers each get their own copy; payloads are `Arc`-wrapped where they
4//! are large to keep broadcast clones cheap.
5//!
6//! The TS `AgentEvent` is a discriminated union on `type`. We model it as a
7//! tagged enum. It is `Debug + Clone` only — `AgentEvent` is not serialized
8//! across the wire (session persistence stores `AgentMessage`s, not events);
9//! tests compare event sequences via `Debug`. This avoids requiring
10//! `Deserialize` on `AssistantMessageEvent` (which is stream-protocol-only).
11
12use rpi_ai::types::{AssistantMessageEvent, ToolResultMessage};
13use std::sync::Arc;
14
15use crate::message::AgentMessage;
16use crate::types::AgentToolResult;
17
18/// An event emitted by the agent loop. Mirrors TS `AgentEvent` (tagged on
19/// `type`). All `AgentMessage` payloads are owned (cloned) so broadcast
20/// subscribers get independent copies.
21#[derive(Debug, Clone)]
22pub enum AgentEvent {
23 /// Emitted once at the start of a run, before `turn_start`.
24 AgentStart,
25 /// Emitted once at the end of a run; carries the new messages produced.
26 AgentEnd { messages: Vec<AgentMessage> },
27 /// Emitted after a retryable provider failure, before the backoff wait.
28 RetryScheduled {
29 /// One-based retry number (the initial request is not counted).
30 attempt: u32,
31 /// Maximum number of retries allowed for this request.
32 max_retries: u32,
33 /// Backoff duration before the next provider request.
34 delay_ms: u64,
35 /// Diagnostic from the failed provider request.
36 error: String,
37 },
38 /// Emitted at the start of each turn (a turn = one assistant response + its
39 /// tool calls/results).
40 TurnStart,
41 /// Emitted after a turn's assistant message + tool results settle. Carries
42 /// the assistant message (as an `AgentMessage`) and the tool-result
43 /// messages produced this turn, in source/ordinal order.
44 TurnEnd {
45 message: AgentMessage,
46 tool_results: Vec<ToolResultMessage>,
47 },
48 /// Emitted when any message (user prompt, assistant response, tool result,
49 /// custom) is appended to the transcript.
50 MessageStart { message: AgentMessage },
51 /// Emitted only for assistant messages, on each streaming delta. Carries
52 /// the underlying `AssistantMessageEvent` plus a snapshot of the partial
53 /// assistant message.
54 MessageUpdate {
55 message: AgentMessage,
56 assistant_message_event: AssistantMessageEvent,
57 },
58 /// Emitted when a message finishes (complement of `MessageStart`).
59 MessageEnd { message: AgentMessage },
60 /// Emitted before a tool call starts executing.
61 ToolExecutionStart {
62 tool_call_id: String,
63 tool_name: String,
64 args: serde_json::Value,
65 },
66 /// Emitted on a partial tool result pushed via `on_update`.
67 ToolExecutionUpdate {
68 tool_call_id: String,
69 tool_name: String,
70 args: serde_json::Value,
71 partial_result: Arc<AgentToolResult>,
72 },
73 /// Emitted when a tool call finishes (success or error). `is_error` marks
74 /// the error path; `result` carries the final tool result.
75 ToolExecutionEnd {
76 tool_call_id: String,
77 tool_name: String,
78 result: AgentToolResult,
79 is_error: bool,
80 },
81}
82
83impl AgentEvent {
84 pub fn type_tag(&self) -> &'static str {
85 match self {
86 AgentEvent::AgentStart => "agent_start",
87 AgentEvent::AgentEnd { .. } => "agent_end",
88 AgentEvent::RetryScheduled { .. } => "retry_scheduled",
89 AgentEvent::TurnStart => "turn_start",
90 AgentEvent::TurnEnd { .. } => "turn_end",
91 AgentEvent::MessageStart { .. } => "message_start",
92 AgentEvent::MessageUpdate { .. } => "message_update",
93 AgentEvent::MessageEnd { .. } => "message_end",
94 AgentEvent::ToolExecutionStart { .. } => "tool_execution_start",
95 AgentEvent::ToolExecutionUpdate { .. } => "tool_execution_update",
96 AgentEvent::ToolExecutionEnd { .. } => "tool_execution_end",
97 }
98 }
99
100 /// True for the terminal `AgentEnd` event.
101 pub fn is_terminal(&self) -> bool {
102 matches!(self, AgentEvent::AgentEnd { .. })
103 }
104}
105
106/// Sink the loop pushes events into. Mirrors TS `AgentEventSink =
107/// (event: AgentEvent) => Promise<void> | void`. Implementations:
108/// [`CollectorEmitter`] (tests/drain), [`BroadcastEmitter`] (live `Agent`).
109///
110/// Two emit surfaces:
111/// - [`AgentEmitter::emit`] — async, awaited in event order by the loop.
112/// - [`AgentEmitter::try_emit`] — sync, non-blocking, used by tool `on_update`
113/// callbacks (which are `&dyn Fn` and cannot await). For `CollectorEmitter`
114/// this pushes under the mutex; for `BroadcastEmitter` it's `tx.send`.
115/// Order is preserved because `try_emit` is only ever called from within a
116/// single tool's `execute`, and `emit` is awaited between tool phases.
117pub trait AgentEmitter: Send + Sync {
118 /// Async emit — awaited by the loop so event order matches emit call order.
119 fn emit(&self, event: AgentEvent) -> futures::future::BoxFuture<'static, ()>;
120
121 /// Sync non-blocking emit for the tool `on_update` path. Must never panic
122 /// and must never block (it's called from a `&dyn Fn` closure inside
123 /// `execute`). Default impl is a no-op so custom emitters opt in.
124 fn try_emit(&self, _event: AgentEvent) {}
125}
126
127/// An emitter that fans out to `broadcast::Sender<AgentEvent>` subscribers.
128pub struct BroadcastEmitter {
129 tx: tokio::sync::broadcast::Sender<AgentEvent>,
130}
131
132impl BroadcastEmitter {
133 pub fn new(buffer: usize) -> (Self, tokio::sync::broadcast::Receiver<AgentEvent>) {
134 let (tx, rx) = tokio::sync::broadcast::channel(buffer);
135 (Self { tx }, rx)
136 }
137
138 pub fn from_sender(tx: tokio::sync::broadcast::Sender<AgentEvent>) -> Self {
139 Self { tx }
140 }
141
142 pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<AgentEvent> {
143 self.tx.subscribe()
144 }
145
146 pub fn try_emit(&self, event: AgentEvent) {
147 let _ = self.tx.send(event);
148 }
149}
150
151impl AgentEmitter for BroadcastEmitter {
152 fn emit(&self, event: AgentEvent) -> futures::future::BoxFuture<'static, ()> {
153 let _ = self.tx.send(event);
154 Box::pin(async {})
155 }
156 fn try_emit(&self, event: AgentEvent) {
157 let _ = self.tx.send(event);
158 }
159}
160
161/// An emitter that collects every event into a `Mutex<Vec<AgentEvent>>`. Used
162/// by tests and by `run_agent_loop` callers that just want the sequence.
163pub struct CollectorEmitter {
164 events: std::sync::Arc<std::sync::Mutex<Vec<AgentEvent>>>,
165}
166
167impl CollectorEmitter {
168 pub fn new() -> (Self, std::sync::Arc<std::sync::Mutex<Vec<AgentEvent>>>) {
169 let events = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
170 (
171 Self {
172 events: Arc::clone(&events),
173 },
174 events,
175 )
176 }
177}
178
179impl Default for CollectorEmitter {
180 fn default() -> Self {
181 let (s, _) = Self::new();
182 s
183 }
184}
185
186impl AgentEmitter for CollectorEmitter {
187 fn emit(&self, event: AgentEvent) -> futures::future::BoxFuture<'static, ()> {
188 self.events.lock().expect("events lock").push(event);
189 Box::pin(async {})
190 }
191 fn try_emit(&self, event: AgentEvent) {
192 self.events.lock().expect("events lock").push(event);
193 }
194}