Skip to main content

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}