orcs_component/emitter.rs
1//! Event emitter trait for Components.
2//!
3//! This trait defines the interface for Components to emit events.
4//! The concrete implementation is provided by `orcs-runtime`.
5//!
6//! # Usage
7//!
8//! ```ignore
9//! impl Component for MyComponent {
10//! fn set_emitter(&mut self, emitter: Box<dyn Emitter>) {
11//! self.emitter = Some(emitter);
12//! }
13//! }
14//!
15//! // Later, emit output
16//! if let Some(emitter) = &self.emitter {
17//! emitter.emit_output("Task completed");
18//! }
19//! ```
20
21use std::fmt::Debug;
22
23/// Trait for emitting events from Components.
24///
25/// Components receive an implementation of this trait via
26/// [`Component::set_emitter`](crate::Component::set_emitter).
27///
28/// The emitter allows Components to:
29/// - Send output to the user (via IO)
30/// - Broadcast signals to other Components
31pub trait Emitter: Send + Sync + Debug {
32 /// Emits an output message (info level).
33 ///
34 /// The message will be displayed to the user via IOBridge.
35 fn emit_output(&self, message: &str);
36
37 /// Emits an output message with a specific level.
38 ///
39 /// # Arguments
40 ///
41 /// * `message` - The message to display
42 /// * `level` - Log level ("info", "warn", "error")
43 fn emit_output_with_level(&self, message: &str, level: &str);
44
45 /// Emits a custom event (broadcast to all channels).
46 ///
47 /// Creates an Extension event with the given category and broadcasts
48 /// it to all registered channels. Channels subscribed to the matching
49 /// Extension category will process it.
50 ///
51 /// # Arguments
52 ///
53 /// * `category` - Extension kind string (e.g., "tool:result")
54 /// * `operation` - Operation name (e.g., "complete")
55 /// * `payload` - Event payload data
56 ///
57 /// # Returns
58 ///
59 /// `true` if the event was broadcast successfully.
60 fn emit_event(&self, _category: &str, _operation: &str, _payload: serde_json::Value) -> bool {
61 false
62 }
63
64 /// Returns the most recent `n` Board entries as JSON values.
65 ///
66 /// The Board is a shared rolling buffer of recent Output and Extension
67 /// events. Components can query it to see what other components have
68 /// emitted recently.
69 ///
70 /// Default implementation returns an empty vec (no board attached).
71 ///
72 /// # Arguments
73 ///
74 /// * `n` - Maximum number of entries to return
75 fn board_recent(&self, _n: usize) -> Vec<serde_json::Value> {
76 Vec::new()
77 }
78
79 /// Sends a synchronous RPC request to another Component.
80 ///
81 /// Routes via EventBus to the target Component's `on_request` handler
82 /// and returns the response. Blocks the calling thread until response
83 /// is received or timeout expires.
84 ///
85 /// # Arguments
86 ///
87 /// * `target` - Target Component name (e.g., "skill-manager")
88 /// * `operation` - Operation name (e.g., "list", "activate")
89 /// * `payload` - Request payload
90 /// * `timeout_ms` - Optional timeout override (default: 30s)
91 ///
92 /// # Returns
93 ///
94 /// Response value from the target Component, or error string.
95 ///
96 /// # Default Implementation
97 ///
98 /// Returns error (not supported without runtime wiring).
99 fn request(
100 &self,
101 _target: &str,
102 _operation: &str,
103 _payload: serde_json::Value,
104 _timeout_ms: Option<u64>,
105 ) -> Result<serde_json::Value, String> {
106 Err("request not supported".into())
107 }
108
109 /// Checks whether a target Component is alive (its runner is still running).
110 ///
111 /// Uses the EventBus channel handle to determine liveness without sending
112 /// a request. This is a best-effort check — the component may die
113 /// immediately after returning `true` (TOCTOU inherent in concurrent systems).
114 ///
115 /// # Arguments
116 ///
117 /// * `target_fqn` - Fully qualified name or short name of the target Component
118 ///
119 /// # Default Implementation
120 ///
121 /// Returns `false` (no runtime wiring available).
122 fn is_alive(&self, _target_fqn: &str) -> bool {
123 false
124 }
125
126 /// Clones the emitter into a boxed trait object.
127 ///
128 /// This allows storing the emitter in the Component.
129 fn clone_box(&self) -> Box<dyn Emitter>;
130}
131
132impl Clone for Box<dyn Emitter> {
133 fn clone(&self) -> Self {
134 self.clone_box()
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[derive(Debug, Clone)]
143 struct MockEmitter;
144
145 impl Emitter for MockEmitter {
146 fn emit_output(&self, _message: &str) {
147 // No-op for testing
148 }
149
150 fn emit_output_with_level(&self, _message: &str, _level: &str) {
151 // No-op for testing
152 }
153
154 fn clone_box(&self) -> Box<dyn Emitter> {
155 Box::new(self.clone())
156 }
157 }
158
159 #[test]
160 fn mock_emitter_works() {
161 let emitter: Box<dyn Emitter> = Box::new(MockEmitter);
162 emitter.emit_output("hello");
163 emitter.emit_output_with_level("warning", "warn");
164 }
165
166 #[test]
167 fn emitter_clone() {
168 let emitter: Box<dyn Emitter> = Box::new(MockEmitter);
169 let cloned = emitter.clone();
170 cloned.emit_output("from clone");
171 }
172}