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}