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 at the start of each turn (a turn = one assistant response + its
28 /// tool calls/results).
29 TurnStart,
30 /// Emitted after a turn's assistant message + tool results settle. Carries
31 /// the assistant message (as an `AgentMessage`) and the tool-result
32 /// messages produced this turn, in source/ordinal order.
33 TurnEnd {
34 message: AgentMessage,
35 tool_results: Vec<ToolResultMessage>,
36 },
37 /// Emitted when any message (user prompt, assistant response, tool result,
38 /// custom) is appended to the transcript.
39 MessageStart { message: AgentMessage },
40 /// Emitted only for assistant messages, on each streaming delta. Carries
41 /// the underlying `AssistantMessageEvent` plus a snapshot of the partial
42 /// assistant message.
43 MessageUpdate {
44 message: AgentMessage,
45 assistant_message_event: AssistantMessageEvent,
46 },
47 /// Emitted when a message finishes (complement of `MessageStart`).
48 MessageEnd { message: AgentMessage },
49 /// Emitted before a tool call starts executing.
50 ToolExecutionStart {
51 tool_call_id: String,
52 tool_name: String,
53 args: serde_json::Value,
54 },
55 /// Emitted on a partial tool result pushed via `on_update`.
56 ToolExecutionUpdate {
57 tool_call_id: String,
58 tool_name: String,
59 args: serde_json::Value,
60 partial_result: Arc<AgentToolResult>,
61 },
62 /// Emitted when a tool call finishes (success or error). `is_error` marks
63 /// the error path; `result` carries the final tool result.
64 ToolExecutionEnd {
65 tool_call_id: String,
66 tool_name: String,
67 result: AgentToolResult,
68 is_error: bool,
69 },
70}
71
72impl AgentEvent {
73 pub fn type_tag(&self) -> &'static str {
74 match self {
75 AgentEvent::AgentStart => "agent_start",
76 AgentEvent::AgentEnd { .. } => "agent_end",
77 AgentEvent::TurnStart => "turn_start",
78 AgentEvent::TurnEnd { .. } => "turn_end",
79 AgentEvent::MessageStart { .. } => "message_start",
80 AgentEvent::MessageUpdate { .. } => "message_update",
81 AgentEvent::MessageEnd { .. } => "message_end",
82 AgentEvent::ToolExecutionStart { .. } => "tool_execution_start",
83 AgentEvent::ToolExecutionUpdate { .. } => "tool_execution_update",
84 AgentEvent::ToolExecutionEnd { .. } => "tool_execution_end",
85 }
86 }
87
88 /// True for the terminal `AgentEnd` event.
89 pub fn is_terminal(&self) -> bool {
90 matches!(self, AgentEvent::AgentEnd { .. })
91 }
92}
93
94/// Sink the loop pushes events into. Mirrors TS `AgentEventSink =
95/// (event: AgentEvent) => Promise<void> | void`. Implementations:
96/// [`CollectorEmitter`] (tests/drain), [`BroadcastEmitter`] (live `Agent`).
97///
98/// Two emit surfaces:
99/// - [`AgentEmitter::emit`] — async, awaited in event order by the loop.
100/// - [`AgentEmitter::try_emit`] — sync, non-blocking, used by tool `on_update`
101/// callbacks (which are `&dyn Fn` and cannot await). For `CollectorEmitter`
102/// this pushes under the mutex; for `BroadcastEmitter` it's `tx.send`.
103/// Order is preserved because `try_emit` is only ever called from within a
104/// single tool's `execute`, and `emit` is awaited between tool phases.
105pub trait AgentEmitter: Send + Sync {
106 /// Async emit — awaited by the loop so event order matches emit call order.
107 fn emit(&self, event: AgentEvent) -> futures::future::BoxFuture<'static, ()>;
108
109 /// Sync non-blocking emit for the tool `on_update` path. Must never panic
110 /// and must never block (it's called from a `&dyn Fn` closure inside
111 /// `execute`). Default impl is a no-op so custom emitters opt in.
112 fn try_emit(&self, _event: AgentEvent) {}
113}
114
115/// An emitter that fans out to `broadcast::Sender<AgentEvent>` subscribers.
116pub struct BroadcastEmitter {
117 tx: tokio::sync::broadcast::Sender<AgentEvent>,
118}
119
120impl BroadcastEmitter {
121 pub fn new(buffer: usize) -> (Self, tokio::sync::broadcast::Receiver<AgentEvent>) {
122 let (tx, rx) = tokio::sync::broadcast::channel(buffer);
123 (Self { tx }, rx)
124 }
125
126 pub fn from_sender(tx: tokio::sync::broadcast::Sender<AgentEvent>) -> Self {
127 Self { tx }
128 }
129
130 pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<AgentEvent> {
131 self.tx.subscribe()
132 }
133
134 pub fn try_emit(&self, event: AgentEvent) {
135 let _ = self.tx.send(event);
136 }
137}
138
139impl AgentEmitter for BroadcastEmitter {
140 fn emit(&self, event: AgentEvent) -> futures::future::BoxFuture<'static, ()> {
141 let _ = self.tx.send(event);
142 Box::pin(async {})
143 }
144 fn try_emit(&self, event: AgentEvent) {
145 let _ = self.tx.send(event);
146 }
147}
148
149/// An emitter that collects every event into a `Mutex<Vec<AgentEvent>>`. Used
150/// by tests and by `run_agent_loop` callers that just want the sequence.
151pub struct CollectorEmitter {
152 events: std::sync::Arc<std::sync::Mutex<Vec<AgentEvent>>>,
153}
154
155impl CollectorEmitter {
156 pub fn new() -> (Self, std::sync::Arc<std::sync::Mutex<Vec<AgentEvent>>>) {
157 let events = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
158 (Self { events: Arc::clone(&events) }, events)
159 }
160}
161
162impl Default for CollectorEmitter {
163 fn default() -> Self {
164 let (s, _) = Self::new();
165 s
166 }
167}
168
169impl AgentEmitter for CollectorEmitter {
170 fn emit(&self, event: AgentEvent) -> futures::future::BoxFuture<'static, ()> {
171 self.events.lock().expect("events lock").push(event);
172 Box::pin(async {})
173 }
174 fn try_emit(&self, event: AgentEvent) {
175 self.events.lock().expect("events lock").push(event);
176 }
177}