orcs_types/id.rs
1//! Identifier types for ORCS.
2//!
3//! All identifiers are UUID-based for network compatibility and
4//! cloud readiness.
5
6use serde::{Deserialize, Serialize};
7use uuid::{uuid, Uuid};
8
9/// ORCS namespace UUID for deterministic UUID v5 generation.
10///
11/// This UUID is used as the namespace for generating deterministic
12/// UUIDs for builtin components via UUID v5 (SHA-1 based).
13const ORCS_NAMESPACE: Uuid = uuid!("d81843c2-9146-43ca-b016-bee57a14762f");
14
15/// Identifier for a Component in the ORCS architecture.
16///
17/// A Component is a functional domain boundary that communicates
18/// via the EventBus. Examples include:
19///
20/// - `builtin::llm` - LLM integration (chat, complete, embed)
21/// - `builtin::tool` - Tool execution (read, write, bash)
22/// - `builtin::hil` - Human-in-the-loop (approve, reject)
23/// - `plugin::my-tool` - User-defined extensions
24///
25/// # UUID Strategy
26///
27/// - **Builtin components**: Use UUID v5 (deterministic from name)
28/// - **Custom components**: Use UUID v4 (random)
29///
30/// This ensures builtin components have consistent UUIDs across
31/// processes and machines, enabling reliable routing and comparison.
32///
33/// # Equality Semantics
34///
35/// `PartialEq` compares all fields including UUID. For FQN-only
36/// comparison (ignoring UUID), use [`fqn_eq`](Self::fqn_eq).
37///
38/// # Example
39///
40/// ```
41/// use orcs_types::ComponentId;
42///
43/// // Builtin: deterministic UUID
44/// let llm1 = ComponentId::builtin("llm");
45/// let llm2 = ComponentId::builtin("llm");
46/// assert_eq!(llm1, llm2); // Same UUID, same component
47///
48/// // Custom: random UUID per instance
49/// let p1 = ComponentId::new("plugin", "tool");
50/// let p2 = ComponentId::new("plugin", "tool");
51/// assert_ne!(p1, p2); // Different UUIDs
52/// assert!(p1.fqn_eq(&p2)); // But same FQN
53/// ```
54#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
55pub struct ComponentId {
56 /// Globally unique identifier.
57 pub uuid: Uuid,
58 /// Namespace (e.g., "builtin", "plugin", "wasm").
59 pub namespace: String,
60 /// Component name within namespace.
61 pub name: String,
62}
63
64impl ComponentId {
65 /// Creates a new [`ComponentId`] with a random UUID v4.
66 ///
67 /// Use this for custom/plugin components where each instance
68 /// should have a unique identity.
69 ///
70 /// # Example
71 ///
72 /// ```
73 /// use orcs_types::ComponentId;
74 ///
75 /// let plugin = ComponentId::new("plugin", "my-tool");
76 /// assert_eq!(plugin.namespace, "plugin");
77 /// assert_eq!(plugin.name, "my-tool");
78 /// assert_eq!(plugin.fqn(), "plugin::my-tool");
79 /// ```
80 #[must_use]
81 pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Self {
82 Self {
83 uuid: Uuid::new_v4(),
84 namespace: namespace.into(),
85 name: name.into(),
86 }
87 }
88
89 /// Creates a builtin component ID with a deterministic UUID v5.
90 ///
91 /// The UUID is derived from the ORCS namespace UUID and the
92 /// component name using SHA-1. This ensures:
93 ///
94 /// - Same name always produces same UUID
95 /// - Different names produce different UUIDs
96 /// - UUIDs are consistent across processes/machines
97 ///
98 /// # Example
99 ///
100 /// ```
101 /// use orcs_types::ComponentId;
102 ///
103 /// let llm1 = ComponentId::builtin("llm");
104 /// let llm2 = ComponentId::builtin("llm");
105 /// let tool = ComponentId::builtin("tool");
106 ///
107 /// assert_eq!(llm1.uuid, llm2.uuid); // Same name = same UUID
108 /// assert_ne!(llm1.uuid, tool.uuid); // Different name = different UUID
109 /// assert!(llm1.is_builtin());
110 /// ```
111 #[must_use]
112 pub fn builtin(name: impl Into<String>) -> Self {
113 let name = name.into();
114 Self {
115 uuid: Uuid::new_v5(&ORCS_NAMESPACE, name.as_bytes()),
116 namespace: "builtin".to_string(),
117 name,
118 }
119 }
120
121 /// Returns the fully qualified name in `namespace::name` format.
122 ///
123 /// # Example
124 ///
125 /// ```
126 /// use orcs_types::ComponentId;
127 ///
128 /// let id = ComponentId::builtin("llm");
129 /// assert_eq!(id.fqn(), "builtin::llm");
130 /// ```
131 #[must_use]
132 pub fn fqn(&self) -> String {
133 format!("{}::{}", self.namespace, self.name)
134 }
135
136 /// Compares two [`ComponentId`]s by FQN only, ignoring UUID.
137 ///
138 /// This is useful when you want to check if two components
139 /// represent the same logical component, even if they were
140 /// created as separate instances.
141 ///
142 /// # Example
143 ///
144 /// ```
145 /// use orcs_types::ComponentId;
146 ///
147 /// let p1 = ComponentId::new("plugin", "tool");
148 /// let p2 = ComponentId::new("plugin", "tool");
149 ///
150 /// assert_ne!(p1, p2); // Different UUIDs
151 /// assert!(p1.fqn_eq(&p2)); // Same FQN
152 /// ```
153 #[must_use]
154 pub fn fqn_eq(&self, other: &Self) -> bool {
155 self.namespace == other.namespace && self.name == other.name
156 }
157
158 /// Checks if this component matches the given namespace and name.
159 ///
160 /// # Example
161 ///
162 /// ```
163 /// use orcs_types::ComponentId;
164 ///
165 /// let llm = ComponentId::builtin("llm");
166 ///
167 /// assert!(llm.matches("builtin", "llm"));
168 /// assert!(!llm.matches("builtin", "tool"));
169 /// ```
170 #[must_use]
171 pub fn matches(&self, namespace: &str, name: &str) -> bool {
172 self.namespace == namespace && self.name == name
173 }
174
175 /// Returns `true` if this is a builtin component.
176 ///
177 /// # Example
178 ///
179 /// ```
180 /// use orcs_types::ComponentId;
181 ///
182 /// let builtin = ComponentId::builtin("llm");
183 /// let plugin = ComponentId::new("plugin", "tool");
184 ///
185 /// assert!(builtin.is_builtin());
186 /// assert!(!plugin.is_builtin());
187 /// ```
188 #[must_use]
189 pub fn is_builtin(&self) -> bool {
190 self.namespace == "builtin"
191 }
192
193 /// Creates a child component ID with a deterministic UUID v5.
194 ///
195 /// The UUID is derived from the ORCS namespace UUID and the
196 /// child name using SHA-1.
197 ///
198 /// # Example
199 ///
200 /// ```
201 /// use orcs_types::ComponentId;
202 ///
203 /// let child = ComponentId::child("worker-1");
204 /// assert_eq!(child.namespace, "child");
205 /// assert_eq!(child.name, "worker-1");
206 /// ```
207 #[must_use]
208 pub fn child(name: impl Into<String>) -> Self {
209 let name = name.into();
210 Self {
211 uuid: Uuid::new_v5(&ORCS_NAMESPACE, format!("child:{}", name).as_bytes()),
212 namespace: "child".to_string(),
213 name,
214 }
215 }
216
217 /// Returns `true` if this is a child component.
218 #[must_use]
219 pub fn is_child(&self) -> bool {
220 self.namespace == "child"
221 }
222}
223
224impl std::fmt::Display for ComponentId {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 write!(f, "{}::{}@{}", self.namespace, self.name, self.uuid)
227 }
228}
229
230/// Identifier for a Principal (actor) in the ORCS system.
231///
232/// A Principal represents "who" is performing an action, separate from
233/// "what permission they have". This separation enables:
234///
235/// - **Dynamic privilege**: Same principal can be Standard or Elevated
236/// - **Audit trails**: Actions are attributed to identifiable actors
237/// - **Network identity**: Principals are unique across distributed systems
238///
239/// # Principal vs Component
240///
241/// - [`PrincipalId`]: Who is acting (human user, system process)
242/// - [`ComponentId`]: What is acting (LLM, Tool, Plugin)
243///
244/// A Component may act on behalf of a Principal, but they are distinct
245/// concepts. A human user (Principal) may trigger an LLM Component,
246/// but the LLM Component itself is not the Principal.
247///
248/// # Example
249///
250/// ```
251/// use orcs_types::PrincipalId;
252///
253/// let user = PrincipalId::new();
254/// let admin = PrincipalId::new();
255///
256/// assert_ne!(user, admin); // Different principals
257/// println!("User: {}", user);
258/// ```
259#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
260pub struct PrincipalId(pub Uuid);
261
262impl PrincipalId {
263 /// Creates a new [`PrincipalId`] with a random UUID v4.
264 ///
265 /// # Example
266 ///
267 /// ```
268 /// use orcs_types::PrincipalId;
269 ///
270 /// let principal = PrincipalId::new();
271 /// println!("Principal: {}", principal);
272 /// ```
273 #[must_use]
274 pub fn new() -> Self {
275 Self(Uuid::new_v4())
276 }
277
278 /// Returns the inner UUID.
279 #[must_use]
280 pub fn uuid(&self) -> Uuid {
281 self.0
282 }
283}
284
285impl Default for PrincipalId {
286 fn default() -> Self {
287 Self::new()
288 }
289}
290
291impl std::fmt::Display for PrincipalId {
292 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
293 write!(f, "principal:{}", self.0)
294 }
295}
296
297/// Identifier for a Channel in the ORCS architecture.
298///
299/// A Channel represents a parallel execution context managed by the
300/// World. Channels form a tree structure where:
301///
302/// - **Primary Channel**: Root channel owned by Human
303/// - **Agent Channels**: Spawned for LLM/Tool execution
304/// - **Background Channels**: Isolated tasks (WASM plugins, etc.)
305///
306/// # Channel Tree
307///
308/// ```text
309/// Primary (Human)
310/// ├── Agent Channel
311/// │ ├── LLM Component
312/// │ └── Tool Component
313/// │ └── Subprocess
314/// └── Background Channel
315/// └── WASM Plugin
316/// ```
317///
318/// # Permission Inheritance
319///
320/// Child channels inherit permissions from their parent.
321/// Children cannot have broader scope than their parent.
322///
323/// # Example
324///
325/// ```
326/// use orcs_types::ChannelId;
327///
328/// let primary = ChannelId::new();
329/// let agent = ChannelId::new();
330///
331/// assert_ne!(primary, agent); // Each channel is unique
332/// println!("Agent channel: {}", agent);
333/// ```
334#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
335pub struct ChannelId(pub Uuid);
336
337#[allow(clippy::new_without_default)] // Default intentionally not implemented - see below
338impl ChannelId {
339 /// Creates a new [`ChannelId`] with a random UUID v4.
340 ///
341 /// # Example
342 ///
343 /// ```
344 /// use orcs_types::ChannelId;
345 ///
346 /// let ch = ChannelId::new();
347 /// println!("Created channel: {}", ch);
348 /// ```
349 #[must_use]
350 pub fn new() -> Self {
351 Self(Uuid::new_v4())
352 }
353
354 /// Returns the inner UUID.
355 ///
356 /// # Example
357 ///
358 /// ```
359 /// use orcs_types::ChannelId;
360 ///
361 /// let ch = ChannelId::new();
362 /// let uuid = ch.uuid();
363 /// assert_eq!(ch.0, uuid);
364 /// ```
365 #[must_use]
366 pub fn uuid(&self) -> Uuid {
367 self.0
368 }
369}
370
371// NOTE: ChannelId intentionally does NOT implement Default.
372// Default::default() would generate a new UUID that is not registered in World,
373// leading to subtle bugs. Use World::create_primary() or World::spawn() instead.
374
375impl std::fmt::Display for ChannelId {
376 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377 write!(f, "ch:{}", self.0)
378 }
379}
380
381/// Identifier for a Request in the ORCS EventBus.
382///
383/// A Request represents a synchronous query from one Component to
384/// another (or broadcast). Each Request expects exactly one Response.
385///
386/// # Request/Response Pattern
387///
388/// ```text
389/// ┌─────────────┐ Request ┌─────────────┐
390/// │ Source │ ─────────► │ Target │
391/// │ Component │ │ Component │
392/// │ │ ◄───────── │ │
393/// └─────────────┘ Response └─────────────┘
394/// ```
395///
396/// # Example
397///
398/// ```
399/// use orcs_types::RequestId;
400///
401/// let req_id = RequestId::new();
402/// println!("Request: {}", req_id);
403/// ```
404#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
405pub struct RequestId(pub Uuid);
406
407#[allow(clippy::new_without_default)] // Default intentionally not implemented - RequestId is generated internally by Request::new()
408impl RequestId {
409 /// Creates a new [`RequestId`] with a random UUID v4.
410 ///
411 /// # Example
412 ///
413 /// ```
414 /// use orcs_types::RequestId;
415 ///
416 /// let id = RequestId::new();
417 /// println!("Request ID: {}", id);
418 /// ```
419 #[must_use]
420 pub fn new() -> Self {
421 Self(Uuid::new_v4())
422 }
423
424 /// Returns the inner UUID.
425 #[must_use]
426 pub fn uuid(&self) -> Uuid {
427 self.0
428 }
429}
430
431// NOTE: RequestId intentionally does NOT implement Default.
432// RequestId is generated internally by Request::new()/try_new().
433// Direct construction should use RequestId::new() explicitly.
434
435impl std::fmt::Display for RequestId {
436 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
437 write!(f, "req:{}", self.0)
438 }
439}
440
441/// Identifier for an Event in the ORCS EventBus.
442///
443/// An Event represents an asynchronous broadcast notification.
444/// Unlike Requests, Events do not expect a Response.
445///
446/// # Event Pattern
447///
448/// ```text
449/// ┌─────────────┐ Event ┌─────────────┐
450/// │ Source │ ─────────► │ Subscriber │
451/// │ Component │ │ Component │
452/// └─────────────┘ └─────────────┘
453/// Event ┌─────────────┐
454/// ─────────► │ Subscriber │
455/// │ Component │
456/// └─────────────┘
457/// ```
458///
459/// # Example
460///
461/// ```
462/// use orcs_types::EventId;
463///
464/// let evt_id = EventId::new();
465/// println!("Event: {}", evt_id);
466/// ```
467#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
468pub struct EventId(pub Uuid);
469
470impl EventId {
471 /// Creates a new [`EventId`] with a random UUID v4.
472 ///
473 /// # Example
474 ///
475 /// ```
476 /// use orcs_types::EventId;
477 ///
478 /// let id = EventId::new();
479 /// println!("Event ID: {}", id);
480 /// ```
481 #[must_use]
482 pub fn new() -> Self {
483 Self(Uuid::new_v4())
484 }
485
486 /// Returns the inner UUID.
487 #[must_use]
488 pub fn uuid(&self) -> Uuid {
489 self.0
490 }
491}
492
493impl Default for EventId {
494 fn default() -> Self {
495 Self::new()
496 }
497}
498
499impl std::fmt::Display for EventId {
500 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
501 write!(f, "evt:{}", self.0)
502 }
503}
504
505// Tests are in lib.rs as integration tests for public API