Skip to main content

orcs_component/
component.rs

1//! Component trait for ORCS EventBus participants.
2//!
3//! Components are the functional units that run on Channels and communicate
4//! via the EventBus. They handle requests, respond to signals, and may
5//! manage child entities internally.
6//!
7//! # Component vs Child
8//!
9//! | Aspect | Component | Child |
10//! |--------|-----------|-------|
11//! | EventBus access | Direct | Via parent |
12//! | Request handling | Yes | No |
13//! | Lifecycle methods | Yes | No |
14//! | Manager capability | Yes | No |
15//!
16//! # Component Hierarchy
17//!
18//! ```text
19//! ┌───────────────────────────────────────────────────────────┐
20//! │                    OrcsEngine (Core)                      │
21//! │  - EventBus dispatch                                      │
22//! │  - Channel/World management                               │
23//! │  - Component Runner (poll-based)                          │
24//! └───────────────────────────────────────────────────────────┘
25//!                            │
26//!        ┌───────────────────┼───────────────────┐
27//!        │                   │                   │
28//!        ▼                   ▼                   ▼
29//!   ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
30//!   │   System    │    │   System    │    │   System    │
31//!   │  Component  │    │  Component  │    │  Component  │
32//!   └─────────────┘    └─────────────┘    └─────────────┘
33//!         │
34//!         │ Internal management (Engine doesn't see this)
35//!         ▼
36//!   ┌─────────────────────────────────────────────────────┐
37//!   │              Child / Agent / Skill                  │
38//!   │              (managed by Component)                 │
39//!   └─────────────────────────────────────────────────────┘
40//! ```
41//!
42//! # Builtin Components
43//!
44//! | Component | Category | Purpose |
45//! |-----------|----------|---------|
46//! | LlmComponent | `Llm` | Chat, complete, embed |
47//! | ToolsComponent | `Tool` | Read, write, edit, bash |
48//! | HilComponent | `Hil` | Human approval/rejection |
49//! | SkillComponent | `Skill` | Auto-triggered skills |
50//!
51//! # Example
52//!
53//! ```
54//! use orcs_component::{Component, ComponentError, Status};
55//! use orcs_event::{Request, Signal, SignalResponse};
56//! use orcs_types::ComponentId;
57//! use serde_json::Value;
58//!
59//! struct EchoComponent {
60//!     id: ComponentId,
61//!     status: Status,
62//! }
63//!
64//! impl Component for EchoComponent {
65//!     fn id(&self) -> &ComponentId {
66//!         &self.id
67//!     }
68//!
69//!     fn status(&self) -> Status {
70//!         self.status
71//!     }
72//!
73//!     fn on_request(&mut self, request: &Request) -> Result<Value, ComponentError> {
74//!         match request.operation.as_str() {
75//!             "echo" => Ok(request.payload.clone()),
76//!             _ => Err(ComponentError::NotSupported(request.operation.clone())),
77//!         }
78//!     }
79//!
80//!     fn on_signal(&mut self, signal: &Signal) -> SignalResponse {
81//!         if signal.is_veto() {
82//!             self.abort();
83//!             SignalResponse::Abort
84//!         } else {
85//!             SignalResponse::Ignored
86//!         }
87//!     }
88//!
89//!     fn abort(&mut self) {
90//!         self.status = Status::Aborted;
91//!     }
92//! }
93//! ```
94
95use crate::{
96    ComponentError, ComponentSnapshot, EventCategory, SnapshotError, Status, StatusDetail,
97};
98use orcs_event::{Request, Signal, SignalResponse};
99use orcs_types::ComponentId;
100use serde_json::Value;
101
102/// Hints from a Component to the Runtime about how it should be spawned.
103///
104/// Components declare these hints; the Runtime inspects them to decide
105/// channel configuration, auth level, and feature enablement.
106///
107/// All fields default to `false` (most restrictive).
108#[derive(Debug, Clone, Default, PartialEq, Eq)]
109pub struct RuntimeHints {
110    /// Route Output events to the IO channel (visible to user).
111    pub output_to_io: bool,
112    /// Request an elevated session (skip HIL for pre-approved commands).
113    pub elevated: bool,
114    /// Enable child process / child component spawning.
115    pub child_spawner: bool,
116}
117
118/// Component trait for EventBus participants.
119///
120/// Components are the primary actors in the ORCS system.
121/// They communicate via the EventBus using Request/Response patterns.
122///
123/// # Required Methods
124///
125/// | Method | Purpose |
126/// |--------|---------|
127/// | `id` | Component identification |
128/// | `status` | Current execution status |
129/// | `subscriptions` | Event categories to receive |
130/// | `on_request` | Handle incoming requests |
131/// | `on_signal` | Handle control signals |
132/// | `abort` | Immediate termination |
133///
134/// # Subscription-based Routing
135///
136/// Components declare which [`EventCategory`] they subscribe to.
137/// The EventBus routes requests only to subscribers of the matching category.
138///
139/// ```text
140/// Component::subscriptions() -> [Hil, Echo]
141///     │
142///     ▼
143/// EventBus::register(component, categories)
144///     │
145///     ▼
146/// Request { category: Hil, operation: "submit" }
147///     │
148///     ▼ (routed only to Hil subscribers)
149/// HilComponent::on_request()
150/// ```
151///
152/// # Signal Handling Contract
153///
154/// Components **must** handle signals, especially:
155///
156/// - **Veto**: Must abort immediately
157/// - **Cancel**: Must check scope and abort if applicable
158///
159/// This is the foundation of "Human as Superpower" -
160/// humans can always interrupt any operation.
161///
162/// # Thread Safety
163///
164/// Components must be `Send + Sync` for concurrent access.
165/// Use interior mutability patterns if needed.
166pub trait Component: Send + Sync {
167    /// Returns the component's identifier.
168    ///
169    /// Used for:
170    /// - Request routing
171    /// - Signal scope checking
172    /// - Logging and debugging
173    fn id(&self) -> &ComponentId;
174
175    /// Returns the event categories this component subscribes to.
176    ///
177    /// The EventBus routes requests only to components that subscribe
178    /// to the request's category.
179    ///
180    /// # Default
181    ///
182    /// Default implementation returns `[Lifecycle]` only.
183    /// Override to receive requests from other categories.
184    ///
185    /// # Example
186    ///
187    /// ```ignore
188    /// fn subscriptions(&self) -> &[EventCategory] {
189    ///     &[EventCategory::Hil, EventCategory::Lifecycle]
190    /// }
191    /// ```
192    fn subscriptions(&self) -> &[EventCategory] {
193        &[EventCategory::Lifecycle]
194    }
195
196    /// Returns subscription entries with optional operation-level filtering.
197    ///
198    /// This method enables fine-grained subscription control. Components can
199    /// declare not only which categories they subscribe to, but also which
200    /// operations within those categories they accept.
201    ///
202    /// The default implementation wraps [`subscriptions()`](Self::subscriptions)
203    /// with wildcard operations (all operations accepted per category).
204    ///
205    /// # Example
206    ///
207    /// ```ignore
208    /// fn subscription_entries(&self) -> Vec<SubscriptionEntry> {
209    ///     vec![
210    ///         SubscriptionEntry::all(EventCategory::UserInput),
211    ///         SubscriptionEntry::with_operations(
212    ///             EventCategory::extension("lua", "Extension"),
213    ///             ["route_response".to_string()],
214    ///         ),
215    ///     ]
216    /// }
217    /// ```
218    fn subscription_entries(&self) -> Vec<orcs_event::SubscriptionEntry> {
219        self.subscriptions()
220            .iter()
221            .map(|c| orcs_event::SubscriptionEntry::all(c.clone()))
222            .collect()
223    }
224
225    /// Returns the current execution status.
226    ///
227    /// Called by the engine to monitor component health.
228    fn status(&self) -> Status;
229
230    /// Returns detailed status information.
231    ///
232    /// Optional - returns `None` by default.
233    /// Override for UI display or debugging.
234    fn status_detail(&self) -> Option<StatusDetail> {
235        None
236    }
237
238    /// Handle an incoming request.
239    ///
240    /// Called when a request is routed to this component.
241    ///
242    /// # Arguments
243    ///
244    /// * `request` - The incoming request
245    ///
246    /// # Returns
247    ///
248    /// - `Ok(Value)` - Successful response payload
249    /// - `Err(ComponentError)` - Operation failed
250    ///
251    /// # Example Operations
252    ///
253    /// | Component | Operations |
254    /// |-----------|------------|
255    /// | LLM | chat, complete, embed |
256    /// | Tools | read, write, edit, bash |
257    /// | HIL | approve, reject |
258    fn on_request(&mut self, request: &Request) -> Result<Value, ComponentError>;
259
260    /// Handle an incoming signal.
261    ///
262    /// Signals are highest priority - process immediately.
263    ///
264    /// # Required Handling
265    ///
266    /// - **Veto**: Must call `abort()` and return `Abort`
267    /// - **Cancel (in scope)**: Should abort
268    /// - **Other**: Check relevance and handle or ignore
269    ///
270    /// # Returns
271    ///
272    /// - `Handled`: Signal was processed
273    /// - `Ignored`: Signal not relevant
274    /// - `Abort`: Component is stopping
275    fn on_signal(&mut self, signal: &Signal) -> SignalResponse;
276
277    /// Immediate abort.
278    ///
279    /// Called on Veto signal. Must:
280    ///
281    /// - Stop all ongoing work immediately
282    /// - Cancel pending operations
283    /// - Release resources
284    /// - Set status to `Aborted`
285    ///
286    /// # Contract
287    ///
288    /// After `abort()`:
289    /// - Component will not receive more requests
290    /// - Any pending responses should be dropped
291    fn abort(&mut self);
292
293    /// Initialize the component with optional configuration.
294    ///
295    /// Called once before the component receives any requests.
296    /// The `config` parameter contains per-component settings from
297    /// `[components.settings.<name>]` in the config file.
298    /// Default implementation ignores the config.
299    ///
300    /// # Errors
301    ///
302    /// Return `Err` if initialization fails.
303    /// The component will not be registered with the EventBus.
304    fn init(&mut self, _config: &serde_json::Value) -> Result<(), ComponentError> {
305        Ok(())
306    }
307
308    /// Shutdown the component.
309    ///
310    /// Called when the engine is stopping.
311    /// Default implementation does nothing.
312    ///
313    /// Should:
314    /// - Clean up resources
315    /// - Persist state if needed
316    /// - Cancel background tasks
317    fn shutdown(&mut self) {
318        // Default: no-op
319    }
320
321    /// Returns runtime hints for this component.
322    ///
323    /// The Runtime uses these hints to configure channel spawning:
324    /// - `output_to_io`: route Output events to the IO channel
325    /// - `elevated`: use an elevated auth session
326    /// - `child_spawner`: enable child spawning capability
327    ///
328    /// # Default
329    ///
330    /// All hints are `false` (most restrictive).
331    fn runtime_hints(&self) -> RuntimeHints {
332        RuntimeHints::default()
333    }
334
335    /// Returns this component as a [`Packageable`](crate::Packageable) if supported.
336    ///
337    /// Override this method in components that implement [`Packageable`](crate::Packageable)
338    /// to enable package management.
339    ///
340    /// # Default
341    ///
342    /// Returns `None` - component does not support packages.
343    fn as_packageable(&self) -> Option<&dyn crate::Packageable> {
344        None
345    }
346
347    /// Returns this component as a mutable [`Packageable`](crate::Packageable) if supported.
348    ///
349    /// Override this method in components that implement [`Packageable`](crate::Packageable)
350    /// to enable package installation/uninstallation.
351    ///
352    /// # Default
353    ///
354    /// Returns `None` - component does not support packages.
355    fn as_packageable_mut(&mut self) -> Option<&mut dyn crate::Packageable> {
356        None
357    }
358
359    /// Sets the event emitter for this component.
360    ///
361    /// The emitter allows the component to emit events to:
362    /// - The owning Channel (for IO output)
363    /// - All Components (via signal broadcast)
364    ///
365    /// Called by `ClientRunner` during component initialization.
366    ///
367    /// # Default
368    ///
369    /// Default implementation does nothing. Override if your component
370    /// needs to emit events.
371    ///
372    /// # Example
373    ///
374    /// ```ignore
375    /// fn set_emitter(&mut self, emitter: Box<dyn Emitter>) {
376    ///     self.emitter = Some(emitter);
377    /// }
378    ///
379    /// // Later, emit output
380    /// if let Some(emitter) = &self.emitter {
381    ///     emitter.emit_output("Result: success");
382    /// }
383    /// ```
384    fn set_emitter(&mut self, _emitter: Box<dyn crate::Emitter>) {
385        // Default: no-op
386    }
387
388    /// Sets the child context for this component.
389    ///
390    /// Override this method if your component needs to spawn children.
391    ///
392    /// # Default
393    ///
394    /// No-op - component does not use child context.
395    fn set_child_context(&mut self, _ctx: Box<dyn crate::ChildContext>) {
396        // Default: no-op
397    }
398
399    // === Snapshot Support ===
400
401    /// Captures the component's current state as a snapshot.
402    ///
403    /// Override this method to enable session persistence for your component.
404    /// The snapshot can later be restored via [`restore`](Self::restore).
405    ///
406    /// # Default
407    ///
408    /// Returns `NotSupported` - component does not support snapshots.
409    ///
410    /// # Example
411    ///
412    /// ```ignore
413    /// fn snapshot(&self) -> Result<ComponentSnapshot, SnapshotError> {
414    ///     ComponentSnapshot::from_state(self.id.fqn(), &self.state)
415    /// }
416    /// ```
417    fn snapshot(&self) -> Result<ComponentSnapshot, SnapshotError> {
418        Err(SnapshotError::NotSupported(self.id().fqn()))
419    }
420
421    /// Restores the component's state from a snapshot.
422    ///
423    /// Override this method to enable session restoration for your component.
424    ///
425    /// # Contract
426    ///
427    /// Implementations **must be idempotent**: calling `restore()` multiple
428    /// times with the same snapshot must produce the same result as calling
429    /// it once. This is a trait-level guarantee that all implementations
430    /// must uphold regardless of internal data structure (e.g., use
431    /// insert/replace, not append).
432    ///
433    /// # Default
434    ///
435    /// Returns `NotSupported` - component does not support snapshots.
436    ///
437    /// # Errors
438    ///
439    /// - `SnapshotError::NotSupported` - Component doesn't support snapshots
440    /// - `SnapshotError::ComponentMismatch` - Snapshot is for different component
441    /// - `SnapshotError::Serialization` - Failed to deserialize state
442    ///
443    /// # Example
444    ///
445    /// ```ignore
446    /// fn restore(&mut self, snapshot: &ComponentSnapshot) -> Result<(), SnapshotError> {
447    ///     self.state = snapshot.to_state()?;
448    ///     Ok(())
449    /// }
450    /// ```
451    fn restore(&mut self, _snapshot: &ComponentSnapshot) -> Result<(), SnapshotError> {
452        Err(SnapshotError::NotSupported(self.id().fqn()))
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use super::*;
459    use orcs_types::{ChannelId, ErrorCode, Principal, PrincipalId};
460
461    struct MockComponent {
462        id: ComponentId,
463        status: Status,
464    }
465
466    impl MockComponent {
467        fn new(name: &str) -> Self {
468            Self {
469                id: ComponentId::builtin(name),
470                status: Status::Idle,
471            }
472        }
473    }
474
475    impl Component for MockComponent {
476        fn id(&self) -> &ComponentId {
477            &self.id
478        }
479
480        fn status(&self) -> Status {
481            self.status
482        }
483
484        fn on_request(&mut self, request: &Request) -> Result<Value, ComponentError> {
485            match request.operation.as_str() {
486                "echo" => Ok(request.payload.clone()),
487                _ => Err(ComponentError::NotSupported(request.operation.clone())),
488            }
489        }
490
491        fn on_signal(&mut self, signal: &Signal) -> SignalResponse {
492            if signal.is_veto() {
493                self.abort();
494                SignalResponse::Abort
495            } else {
496                SignalResponse::Ignored
497            }
498        }
499
500        fn abort(&mut self) {
501            self.status = Status::Aborted;
502        }
503    }
504
505    #[test]
506    fn component_echo() {
507        let mut comp = MockComponent::new("echo");
508        let source = ComponentId::builtin("test");
509        let channel = ChannelId::new();
510        let req = Request::new(
511            EventCategory::Echo,
512            "echo",
513            source,
514            channel,
515            Value::String("hello".into()),
516        );
517
518        assert_eq!(
519            comp.on_request(&req).unwrap(),
520            Value::String("hello".into())
521        );
522    }
523
524    #[test]
525    fn component_not_supported() {
526        let mut comp = MockComponent::new("test");
527        let source = ComponentId::builtin("test");
528        let channel = ChannelId::new();
529        let req = Request::new(EventCategory::Echo, "unknown", source, channel, Value::Null);
530
531        let result = comp.on_request(&req);
532        assert!(result.is_err());
533        assert_eq!(result.unwrap_err().code(), "COMPONENT_NOT_SUPPORTED");
534    }
535
536    #[test]
537    fn component_abort_on_veto() {
538        let mut comp = MockComponent::new("test");
539        let signal = Signal::veto(Principal::User(PrincipalId::new()));
540
541        let resp = comp.on_signal(&signal);
542        assert_eq!(resp, SignalResponse::Abort);
543        assert_eq!(comp.status(), Status::Aborted);
544    }
545
546    #[test]
547    fn component_init_default() {
548        let mut comp = MockComponent::new("test");
549        let empty = serde_json::Value::Object(serde_json::Map::new());
550        assert!(comp.init(&empty).is_ok());
551    }
552
553    #[test]
554    fn component_status_detail_default() {
555        let comp = MockComponent::new("test");
556        assert!(comp.status_detail().is_none());
557    }
558}