Skip to main content

enact_core/kernel/
ids.rs

1//! Execution & Telemetry IDs - Graph-centric observability identifiers
2//!
3//! This module defines the canonical ID types for the enact-core runtime,
4//! aligned with the Execution/Step/Artifact hierarchy.
5//!
6//! ## ID Format
7//! All IDs use prefixed KSUID (K-Sortable Unique IDentifier):
8//! - `exec_[27 alphanumeric chars]` - ExecutionId
9//! - `step_[27 alphanumeric chars]` - StepId
10//! - `artifact_[27 alphanumeric chars]` - ArtifactId
11//!
12//! ## Hierarchy
13//! ```text
14//! ExecutionId (The Run)
15//!   ├── StepId (Type: ToolNode) -> Artifacts
16//!   ├── StepId (Type: LlmNode)  -> Artifacts
17//!   └── StepId (Type: GraphNode "Sub-Agent")
18//!         └── ExecutionId (Nested run)
19//!               └── StepId...
20//! ```
21//!
22//! @see docs/TECHNICAL/01-EXECUTION-TELEMETRY.md
23
24use serde::{Deserialize, Serialize};
25use std::fmt;
26use svix_ksuid::{Ksuid, KsuidLike};
27
28/// Generate a new KSUID
29fn new_ksuid() -> String {
30    Ksuid::new(None, None).to_string()
31}
32
33// =============================================================================
34// ExecutionId - One run of a blueprint (Agent/Graph/Workflow)
35// =============================================================================
36
37/// ExecutionId - Identifies one run of a blueprint
38///
39/// This is the primary unit of billing and audit.
40/// Produces an append-only stream of ExecutionEvents.
41///
42/// Format: `exec_[27-char KSUID]`
43#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
44pub struct ExecutionId(String);
45
46impl ExecutionId {
47    /// Create a new ExecutionId with auto-generated KSUID
48    pub fn new() -> Self {
49        Self(format!("exec_{}", new_ksuid()))
50    }
51
52    /// Create from an existing string (useful for deserialization)
53    pub fn from_string(s: impl Into<String>) -> Self {
54        Self(s.into())
55    }
56
57    /// Get the ID as a string slice
58    pub fn as_str(&self) -> &str {
59        &self.0
60    }
61}
62
63impl Default for ExecutionId {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl fmt::Display for ExecutionId {
70    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71        write!(f, "{}", self.0)
72    }
73}
74
75impl From<String> for ExecutionId {
76    fn from(s: String) -> Self {
77        Self(s)
78    }
79}
80
81impl From<&str> for ExecutionId {
82    fn from(s: &str) -> Self {
83        Self(s.to_string())
84    }
85}
86
87/// RunId - Alias for ExecutionId
88pub type RunId = ExecutionId;
89
90// =============================================================================
91// StepId - A distinct action within an execution
92// =============================================================================
93
94/// StepId - Identifies a distinct action within an execution
95///
96/// In TUI/CLI, appears as a "Sub-Agent" or "Action" performing work.
97/// Internally, this is a graph node execution.
98///
99/// Format: `step_[27-char KSUID]`
100#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
101pub struct StepId(String);
102
103impl StepId {
104    /// Create a new StepId with auto-generated KSUID
105    pub fn new() -> Self {
106        Self(format!("step_{}", new_ksuid()))
107    }
108
109    /// Create from an existing string
110    pub fn from_string(s: impl Into<String>) -> Self {
111        Self(s.into())
112    }
113
114    /// Get the ID as a string slice
115    pub fn as_str(&self) -> &str {
116        &self.0
117    }
118}
119
120impl Default for StepId {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126impl fmt::Display for StepId {
127    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128        write!(f, "{}", self.0)
129    }
130}
131
132impl From<String> for StepId {
133    fn from(s: String) -> Self {
134        Self(s)
135    }
136}
137
138impl From<&str> for StepId {
139    fn from(s: &str) -> Self {
140        Self(s.to_string())
141    }
142}
143
144/// NodeId - Alias for StepId
145pub type NodeId = StepId;
146
147// =============================================================================
148// GraphId - Graph definition identifier (design-time)
149// =============================================================================
150
151/// GraphId - Graph definition identifier
152///
153/// Identifies a static execution blueprint (Nodes + Edges).
154/// Versioned and immutable once published.
155///
156/// Format: `graph_[27-char KSUID]`
157#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
158pub struct GraphId(String);
159
160impl GraphId {
161    pub fn new() -> Self {
162        Self(format!("graph_{}", new_ksuid()))
163    }
164
165    pub fn from_string(s: impl Into<String>) -> Self {
166        Self(s.into())
167    }
168
169    pub fn as_str(&self) -> &str {
170        &self.0
171    }
172}
173
174impl Default for GraphId {
175    fn default() -> Self {
176        Self::new()
177    }
178}
179
180impl fmt::Display for GraphId {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        write!(f, "{}", self.0)
183    }
184}
185
186// =============================================================================
187// ArtifactId - Data produced by a step
188// =============================================================================
189
190/// ArtifactId - Identifies persisted output produced by a step
191///
192/// Examples: Generated Code, PDF Report, Search JSON, Image.
193///
194/// Format: `artifact_[27-char KSUID]`
195#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
196pub struct ArtifactId(String);
197
198impl ArtifactId {
199    pub fn new() -> Self {
200        Self(format!("artifact_{}", new_ksuid()))
201    }
202
203    pub fn from_string(s: impl Into<String>) -> Self {
204        Self(s.into())
205    }
206
207    pub fn as_str(&self) -> &str {
208        &self.0
209    }
210}
211
212impl Default for ArtifactId {
213    fn default() -> Self {
214        Self::new()
215    }
216}
217
218impl fmt::Display for ArtifactId {
219    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
220        write!(f, "{}", self.0)
221    }
222}
223
224// =============================================================================
225// TenantId / UserId - Multi-tenant context
226// =============================================================================
227
228/// TenantId - Multi-tenant isolation identifier
229///
230/// Format: `tenant_[27-char KSUID]`
231#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
232pub struct TenantId(String);
233
234impl TenantId {
235    pub fn new() -> Self {
236        Self(format!("tenant_{}", new_ksuid()))
237    }
238
239    pub fn from_string(s: impl Into<String>) -> Self {
240        Self(s.into())
241    }
242
243    pub fn as_str(&self) -> &str {
244        &self.0
245    }
246}
247
248impl Default for TenantId {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254impl fmt::Display for TenantId {
255    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256        write!(f, "{}", self.0)
257    }
258}
259
260impl From<String> for TenantId {
261    fn from(s: String) -> Self {
262        Self(s)
263    }
264}
265
266impl From<&str> for TenantId {
267    fn from(s: &str) -> Self {
268        Self(s.to_string())
269    }
270}
271
272/// UserId - User identifier
273///
274/// Format: `user_[27-char KSUID]`
275#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
276pub struct UserId(String);
277
278impl UserId {
279    pub fn new() -> Self {
280        Self(format!("user_{}", new_ksuid()))
281    }
282
283    pub fn from_string(s: impl Into<String>) -> Self {
284        Self(s.into())
285    }
286
287    pub fn as_str(&self) -> &str {
288        &self.0
289    }
290}
291
292impl Default for UserId {
293    fn default() -> Self {
294        Self::new()
295    }
296}
297
298impl fmt::Display for UserId {
299    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
300        write!(f, "{}", self.0)
301    }
302}
303
304impl From<String> for UserId {
305    fn from(s: String) -> Self {
306        Self(s)
307    }
308}
309
310impl From<&str> for UserId {
311    fn from(s: &str) -> Self {
312        Self(s.to_string())
313    }
314}
315
316// =============================================================================
317// StepType - What kind of step this is
318// =============================================================================
319
320/// StepType - Defines what the step *is* (internal: NodeType)
321#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
322#[serde(rename_all = "PascalCase")]
323pub enum StepType {
324    /// LLM call step
325    LlmNode,
326    /// Sub-agent / sub-graph execution
327    GraphNode,
328    /// Tool execution
329    ToolNode,
330    /// Deterministic code function
331    FunctionNode,
332    /// Routing decision node
333    RouterNode,
334    /// Conditional branch node
335    BranchNode,
336    /// Loop iteration node
337    LoopNode,
338}
339
340impl fmt::Display for StepType {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        match self {
343            StepType::LlmNode => write!(f, "LlmNode"),
344            StepType::GraphNode => write!(f, "GraphNode"),
345            StepType::ToolNode => write!(f, "ToolNode"),
346            StepType::FunctionNode => write!(f, "FunctionNode"),
347            StepType::RouterNode => write!(f, "RouterNode"),
348            StepType::BranchNode => write!(f, "BranchNode"),
349            StepType::LoopNode => write!(f, "LoopNode"),
350        }
351    }
352}
353
354// =============================================================================
355// CallableType - What kind of callable was invoked
356// =============================================================================
357
358/// CallableType - The type of callable that was invoked
359///
360/// Used for billing, traceability, and audit trails. Unlike `name` which can change,
361/// `callable_id` + `callable_type` provide stable identifiers for cost attribution.
362#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
363#[serde(rename_all = "snake_case")]
364pub enum CallableType {
365    /// Single LLM call, no history
366    Completion,
367    /// Multi-turn conversation
368    Chat,
369    /// Goal-driven with tool use
370    #[default]
371    Agent,
372    /// Compiled graph execution
373    Workflow,
374    /// Ephemeral tasks (title gen, summarization)
375    Background,
376    /// Callable that invokes other callables
377    Composite,
378    /// Tool execution (wraps a tool as callable)
379    Tool,
380    /// Custom/user-defined callable
381    Custom,
382}
383
384impl fmt::Display for CallableType {
385    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
386        match self {
387            CallableType::Completion => write!(f, "completion"),
388            CallableType::Chat => write!(f, "chat"),
389            CallableType::Agent => write!(f, "agent"),
390            CallableType::Workflow => write!(f, "workflow"),
391            CallableType::Background => write!(f, "background"),
392            CallableType::Composite => write!(f, "composite"),
393            CallableType::Tool => write!(f, "tool"),
394            CallableType::Custom => write!(f, "custom"),
395        }
396    }
397}
398
399// =============================================================================
400// Parent Linkage - Polymorphic root for causal tracing
401// =============================================================================
402
403/// ParentType - What kind of entity triggered this execution
404///
405/// Instead of many nullable root fields, we use a single parent pointer
406/// to trace causal origin.
407#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
408#[serde(rename_all = "snake_case")]
409pub enum ParentType {
410    /// User chatted in UI (e.g., msg_1234)
411    UserMessage,
412    /// Scheduled job (e.g., evt_cron_daily)
413    ScheduleEvent,
414    /// A parent agent invoked a sub-agent (e.g., step_abc_789)
415    StepExecution,
416    /// External system trigger (e.g., wh_stripe_hook)
417    Webhook,
418    /// Agent-to-agent request
419    A2aRequest,
420    /// System-initiated (startup, recovery)
421    System,
422    /// Assistant message in a thread
423    AssistantMessage,
424    /// Start of a thread (no parent)
425    ThreadStart,
426    /// Result of a tool execution
427    ToolResult,
428}
429
430impl fmt::Display for ParentType {
431    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
432        match self {
433            ParentType::UserMessage => write!(f, "user_message"),
434            ParentType::ScheduleEvent => write!(f, "schedule_event"),
435            ParentType::StepExecution => write!(f, "step_execution"),
436            ParentType::Webhook => write!(f, "webhook"),
437            ParentType::A2aRequest => write!(f, "a2a_request"),
438            ParentType::System => write!(f, "system"),
439            ParentType::AssistantMessage => write!(f, "assistant_message"),
440            ParentType::ThreadStart => write!(f, "thread_start"),
441            ParentType::ToolResult => write!(f, "tool_result"),
442        }
443    }
444}
445
446/// ParentLink - Traces causal origin of an execution
447///
448/// Every execution has a parent link that describes what triggered it.
449#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
450pub struct ParentLink {
451    /// The ID of the triggering entity
452    pub parent_id: String,
453    /// What kind of entity triggered this
454    pub parent_type: ParentType,
455}
456
457impl ParentLink {
458    /// Create a new ParentLink
459    pub fn new(parent_id: impl Into<String>, parent_type: ParentType) -> Self {
460        Self {
461            parent_id: parent_id.into(),
462            parent_type,
463        }
464    }
465
466    /// Create a ParentLink for a user message trigger
467    pub fn from_user_message(message_id: impl Into<String>) -> Self {
468        Self::new(message_id, ParentType::UserMessage)
469    }
470
471    /// Create a ParentLink for a step execution (sub-agent) trigger
472    pub fn from_step(step_id: &StepId) -> Self {
473        Self::new(step_id.as_str(), ParentType::StepExecution)
474    }
475
476    /// Create a ParentLink for a parent execution (nested execution)
477    pub fn execution(execution_id: ExecutionId) -> Self {
478        Self::new(execution_id.as_str(), ParentType::StepExecution)
479    }
480
481    /// Create a ParentLink for a system trigger
482    pub fn system() -> Self {
483        Self::new("system", ParentType::System)
484    }
485}
486
487// =============================================================================
488// StepSource - Tracks why/how a step was created
489// =============================================================================
490
491/// StepSourceType - Why/how a step was created
492///
493/// Used to track the origin of each step for audit trails and replay.
494/// Follows the same pattern as ParentType but at the step level.
495#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
496#[serde(rename_all = "snake_case")]
497pub enum StepSourceType {
498    /// Part of initial compiled graph (default)
499    #[default]
500    InitialPlan,
501    /// Discovered during agentic execution
502    Discovered,
503    /// Retry of a failed step
504    Retry,
505    /// From user guidance via inbox
506    UserGuidance,
507    /// From tool result suggesting new work
508    ToolResult,
509    /// From LLM output suggesting new work
510    LlmOutput,
511    /// From external agent (A2A)
512    A2aMessage,
513}
514
515impl fmt::Display for StepSourceType {
516    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
517        match self {
518            StepSourceType::InitialPlan => write!(f, "initial_plan"),
519            StepSourceType::Discovered => write!(f, "discovered"),
520            StepSourceType::Retry => write!(f, "retry"),
521            StepSourceType::UserGuidance => write!(f, "user_guidance"),
522            StepSourceType::ToolResult => write!(f, "tool_result"),
523            StepSourceType::LlmOutput => write!(f, "llm_output"),
524            StepSourceType::A2aMessage => write!(f, "a2a_message"),
525        }
526    }
527}
528
529/// StepSource - Tracks the origin of a step
530///
531/// Every step can optionally track why it was created, enabling:
532/// - Full audit trails for discovered steps
533/// - Replay of agentic discovery sequences
534/// - Understanding step chains and dependencies
535///
536/// @see packages/enact-schemas/src/execution.schemas.ts - stepSourceSchema
537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
538#[serde(rename_all = "camelCase")]
539pub struct StepSource {
540    /// Type of source (why was this step created)
541    pub source_type: StepSourceType,
542    /// ID of the step/entity that triggered this step (if any)
543    #[serde(skip_serializing_if = "Option::is_none")]
544    pub triggered_by: Option<String>,
545    /// Human-readable reason for step creation
546    #[serde(skip_serializing_if = "Option::is_none")]
547    pub reason: Option<String>,
548    /// Discovery depth (for discovered steps - how deep in discovery chain)
549    #[serde(skip_serializing_if = "Option::is_none")]
550    pub depth: Option<u32>,
551    /// How this step was spawned (for spawned steps)
552    #[serde(skip_serializing_if = "Option::is_none")]
553    pub spawn_mode: Option<SpawnMode>,
554}
555
556impl StepSource {
557    /// Create a new StepSource
558    pub fn new(source_type: StepSourceType) -> Self {
559        Self {
560            source_type,
561            triggered_by: None,
562            reason: None,
563            depth: None,
564            spawn_mode: None,
565        }
566    }
567
568    /// Create a source for initial plan steps
569    pub fn initial_plan() -> Self {
570        Self::new(StepSourceType::InitialPlan)
571    }
572
573    /// Create a source for discovered steps
574    pub fn discovered(triggered_by: &StepId, reason: impl Into<String>, depth: u32) -> Self {
575        Self {
576            source_type: StepSourceType::Discovered,
577            triggered_by: Some(triggered_by.as_str().to_string()),
578            reason: Some(reason.into()),
579            depth: Some(depth),
580            spawn_mode: None,
581        }
582    }
583
584    /// Create a source for retry steps
585    pub fn retry(original_step_id: &StepId) -> Self {
586        Self {
587            source_type: StepSourceType::Retry,
588            triggered_by: Some(original_step_id.as_str().to_string()),
589            reason: None,
590            depth: None,
591            spawn_mode: None,
592        }
593    }
594
595    /// Create a source for user guidance steps
596    pub fn user_guidance(message_id: impl Into<String>, reason: impl Into<String>) -> Self {
597        Self {
598            source_type: StepSourceType::UserGuidance,
599            triggered_by: Some(message_id.into()),
600            reason: Some(reason.into()),
601            depth: None,
602            spawn_mode: None,
603        }
604    }
605
606    /// Create a source for LLM output discovery
607    pub fn llm_output(step_id: &StepId, reason: impl Into<String>, depth: u32) -> Self {
608        Self {
609            source_type: StepSourceType::LlmOutput,
610            triggered_by: Some(step_id.as_str().to_string()),
611            reason: Some(reason.into()),
612            depth: Some(depth),
613            spawn_mode: None,
614        }
615    }
616
617    /// Create a source for tool result discovery
618    pub fn tool_result(step_id: &StepId, reason: impl Into<String>, depth: u32) -> Self {
619        Self {
620            source_type: StepSourceType::ToolResult,
621            triggered_by: Some(step_id.as_str().to_string()),
622            reason: Some(reason.into()),
623            depth: Some(depth),
624            spawn_mode: None,
625        }
626    }
627
628    /// Set the spawn_mode field
629    pub fn with_spawn_mode(mut self, spawn_mode: SpawnMode) -> Self {
630        self.spawn_mode = Some(spawn_mode);
631        self
632    }
633
634    /// Set the triggered_by field
635    pub fn with_triggered_by(mut self, triggered_by: impl Into<String>) -> Self {
636        self.triggered_by = Some(triggered_by.into());
637        self
638    }
639
640    /// Set the reason field
641    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
642        self.reason = Some(reason.into());
643        self
644    }
645
646    /// Set the depth field
647    pub fn with_depth(mut self, depth: u32) -> Self {
648        self.depth = Some(depth);
649        self
650    }
651}
652
653impl Default for StepSource {
654    fn default() -> Self {
655        Self::initial_plan()
656    }
657}
658
659// =============================================================================
660// SpawnMode - Execution Isolation Control
661// =============================================================================
662
663/// SpawnMode - How a callable spawns child work
664///
665/// Controls execution isolation, inbox routing, and cancellation behavior.
666/// Every callable can spawn children - SpawnMode determines HOW.
667///
668/// @see packages/enact-schemas/src/execution.schemas.ts - spawnModeSchema
669/// @see docs/TECHNICAL/32-SPAWN-MODE.md
670#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
671#[serde(tag = "mode", rename_all = "snake_case")]
672pub enum SpawnMode {
673    /// Child work joins parent's pending_steps queue (same ExecutionId)
674    /// - Same execution context
675    /// - Same inbox
676    /// - Same policies
677    #[default]
678    Inline,
679
680    /// Child work runs as isolated execution (new ExecutionId with ParentLink)
681    Child {
682        /// Run in background (don't block parent)
683        #[serde(default)]
684        background: bool,
685
686        /// Inherit parent's inbox (forward messages)
687        #[serde(default)]
688        inherit_inbox: bool,
689
690        /// Override policies for child (JSON object)
691        #[serde(skip_serializing_if = "Option::is_none")]
692        policies: Option<serde_json::Value>,
693    },
694}
695
696impl SpawnMode {
697    /// Create inline spawn mode (default)
698    pub fn inline() -> Self {
699        SpawnMode::Inline
700    }
701
702    /// Create child spawn mode with options
703    pub fn child(background: bool, inherit_inbox: bool) -> Self {
704        SpawnMode::Child {
705            background,
706            inherit_inbox,
707            policies: None,
708        }
709    }
710
711    /// Create child spawn mode with custom policies
712    pub fn child_with_policies(
713        background: bool,
714        inherit_inbox: bool,
715        policies: serde_json::Value,
716    ) -> Self {
717        SpawnMode::Child {
718            background,
719            inherit_inbox,
720            policies: Some(policies),
721        }
722    }
723
724    /// Check if this is inline mode
725    pub fn is_inline(&self) -> bool {
726        matches!(self, SpawnMode::Inline)
727    }
728
729    /// Check if this is child mode
730    pub fn is_child(&self) -> bool {
731        matches!(self, SpawnMode::Child { .. })
732    }
733
734    /// Check if child should run in background
735    pub fn is_background(&self) -> bool {
736        matches!(
737            self,
738            SpawnMode::Child {
739                background: true,
740                ..
741            }
742        )
743    }
744
745    /// Check if child should inherit parent's inbox
746    pub fn inherits_inbox(&self) -> bool {
747        matches!(
748            self,
749            SpawnMode::Child {
750                inherit_inbox: true,
751                ..
752            }
753        )
754    }
755}
756
757impl fmt::Display for SpawnMode {
758    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
759        match self {
760            SpawnMode::Inline => write!(f, "inline"),
761            SpawnMode::Child {
762                background,
763                inherit_inbox,
764                ..
765            } => {
766                write!(
767                    f,
768                    "child(background={}, inherit_inbox={})",
769                    background, inherit_inbox
770                )
771            }
772        }
773    }
774}
775
776// =============================================================================
777// CancellationPolicy - Child Execution Lifecycle
778// =============================================================================
779
780/// CancellationPolicy - What happens to children when parent is cancelled
781///
782/// Controls the lifecycle relationship between parent and child executions.
783///
784/// @see packages/enact-schemas/src/execution.schemas.ts - cancellationPolicySchema
785/// @see docs/TECHNICAL/32-SPAWN-MODE.md
786#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
787#[serde(rename_all = "snake_case")]
788pub enum CancellationPolicy {
789    /// Cancel all children when parent cancelled (default)
790    #[default]
791    CascadeCancel,
792
793    /// Let children complete, parent waits for them
794    WaitForChildren,
795
796    /// Detach children (they continue independently)
797    Detach,
798}
799
800impl fmt::Display for CancellationPolicy {
801    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
802        match self {
803            CancellationPolicy::CascadeCancel => write!(f, "cascade_cancel"),
804            CancellationPolicy::WaitForChildren => write!(f, "wait_for_children"),
805            CancellationPolicy::Detach => write!(f, "detach"),
806        }
807    }
808}
809
810// =============================================================================
811// ThreadId / MessageId - Conversation hierarchy
812// =============================================================================
813
814/// ThreadId - Conversation thread identifier
815///
816/// Groups messages into a single conversation context.
817///
818/// Format: `thread_[27-char KSUID]`
819#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
820pub struct ThreadId(String);
821
822impl ThreadId {
823    pub fn new() -> Self {
824        Self(format!("thread_{}", new_ksuid()))
825    }
826
827    pub fn from_string(s: impl Into<String>) -> Self {
828        Self(s.into())
829    }
830
831    pub fn as_str(&self) -> &str {
832        &self.0
833    }
834}
835
836impl Default for ThreadId {
837    fn default() -> Self {
838        Self::new()
839    }
840}
841
842impl fmt::Display for ThreadId {
843    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
844        write!(f, "{}", self.0)
845    }
846}
847
848impl From<String> for ThreadId {
849    fn from(s: String) -> Self {
850        Self(s)
851    }
852}
853
854impl From<&str> for ThreadId {
855    fn from(s: &str) -> Self {
856        Self(s.to_string())
857    }
858}
859
860/// MessageId - Individual message identifier
861///
862/// Identifies a single message within a thread.
863///
864/// Format: `msg_[27-char KSUID]`
865#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
866pub struct MessageId(String);
867
868impl MessageId {
869    pub fn new() -> Self {
870        Self(format!("msg_{}", new_ksuid()))
871    }
872
873    pub fn from_string(s: impl Into<String>) -> Self {
874        Self(s.into())
875    }
876
877    pub fn as_str(&self) -> &str {
878        &self.0
879    }
880}
881
882impl Default for MessageId {
883    fn default() -> Self {
884        Self::new()
885    }
886}
887
888impl fmt::Display for MessageId {
889    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
890        write!(f, "{}", self.0)
891    }
892}
893
894impl From<String> for MessageId {
895    fn from(s: String) -> Self {
896        Self(s)
897    }
898}
899
900impl From<&str> for MessageId {
901    fn from(s: &str) -> Self {
902        Self(s.to_string())
903    }
904}
905
906// =============================================================================
907// ID Prefix Constants
908// =============================================================================
909
910/// ID prefix constants for validation and generation
911pub mod prefixes {
912    pub const EXECUTION: &str = "exec_";
913    pub const STEP: &str = "step_";
914    pub const ARTIFACT: &str = "artifact_";
915    pub const GRAPH: &str = "graph_";
916    pub const TENANT: &str = "tenant_";
917    pub const USER: &str = "user_";
918    pub const EVENT: &str = "evt_";
919    pub const DECISION: &str = "dec_";
920    pub const CONTROL: &str = "ctrl_";
921    pub const THREAD: &str = "thread_";
922    pub const MESSAGE: &str = "msg_";
923}
924
925#[cfg(test)]
926mod tests {
927    use super::*;
928    use std::collections::HashSet;
929
930    // =========================================================================
931    // ExecutionId Tests
932    // =========================================================================
933
934    #[test]
935    fn test_execution_id_new_has_correct_prefix() {
936        let id = ExecutionId::new();
937        assert!(
938            id.as_str().starts_with("exec_"),
939            "ExecutionId should start with 'exec_'"
940        );
941    }
942
943    #[test]
944    fn test_execution_id_new_unique() {
945        let id1 = ExecutionId::new();
946        let id2 = ExecutionId::new();
947        assert_ne!(id1, id2, "Each new ExecutionId should be unique");
948    }
949
950    #[test]
951    fn test_execution_id_from_string() {
952        let id = ExecutionId::from_string("exec_custom123");
953        assert_eq!(id.as_str(), "exec_custom123");
954    }
955
956    #[test]
957    fn test_execution_id_from_string_owned() {
958        let id = ExecutionId::from_string(String::from("exec_owned"));
959        assert_eq!(id.as_str(), "exec_owned");
960    }
961
962    #[test]
963    fn test_execution_id_display() {
964        let id = ExecutionId::from_string("exec_display_test");
965        assert_eq!(format!("{}", id), "exec_display_test");
966    }
967
968    #[test]
969    fn test_execution_id_default() {
970        let id = ExecutionId::default();
971        assert!(id.as_str().starts_with("exec_"));
972    }
973
974    #[test]
975    fn test_execution_id_from_string_trait() {
976        let id: ExecutionId = String::from("exec_from_string").into();
977        assert_eq!(id.as_str(), "exec_from_string");
978    }
979
980    #[test]
981    fn test_execution_id_from_str_trait() {
982        let id: ExecutionId = "exec_from_str".into();
983        assert_eq!(id.as_str(), "exec_from_str");
984    }
985
986    #[test]
987    fn test_execution_id_clone() {
988        let id1 = ExecutionId::new();
989        let id2 = id1.clone();
990        assert_eq!(id1, id2);
991    }
992
993    #[test]
994    fn test_execution_id_hash() {
995        let id1 = ExecutionId::from_string("exec_hash_test");
996        let id2 = ExecutionId::from_string("exec_hash_test");
997        let mut set = HashSet::new();
998        set.insert(id1);
999        assert!(set.contains(&id2));
1000    }
1001
1002    #[test]
1003    fn test_execution_id_debug() {
1004        let id = ExecutionId::from_string("exec_debug");
1005        let debug_str = format!("{:?}", id);
1006        assert!(debug_str.contains("exec_debug"));
1007    }
1008
1009    #[test]
1010    fn test_execution_id_serde() {
1011        let id = ExecutionId::from_string("exec_serde_test");
1012        let serialized = serde_json::to_string(&id).unwrap();
1013        let deserialized: ExecutionId = serde_json::from_str(&serialized).unwrap();
1014        assert_eq!(id, deserialized);
1015    }
1016
1017    // =========================================================================
1018    // StepId Tests
1019    // =========================================================================
1020
1021    #[test]
1022    fn test_step_id_new_has_correct_prefix() {
1023        let id = StepId::new();
1024        assert!(
1025            id.as_str().starts_with("step_"),
1026            "StepId should start with 'step_'"
1027        );
1028    }
1029
1030    #[test]
1031    fn test_step_id_new_unique() {
1032        let id1 = StepId::new();
1033        let id2 = StepId::new();
1034        assert_ne!(id1, id2);
1035    }
1036
1037    #[test]
1038    fn test_step_id_from_string() {
1039        let id = StepId::from_string("step_custom");
1040        assert_eq!(id.as_str(), "step_custom");
1041    }
1042
1043    #[test]
1044    fn test_step_id_display() {
1045        let id = StepId::from_string("step_display");
1046        assert_eq!(format!("{}", id), "step_display");
1047    }
1048
1049    #[test]
1050    fn test_step_id_default() {
1051        let id = StepId::default();
1052        assert!(id.as_str().starts_with("step_"));
1053    }
1054
1055    #[test]
1056    fn test_step_id_from_traits() {
1057        let id1: StepId = String::from("step_string").into();
1058        let id2: StepId = "step_str".into();
1059        assert_eq!(id1.as_str(), "step_string");
1060        assert_eq!(id2.as_str(), "step_str");
1061    }
1062
1063    #[test]
1064    fn test_step_id_hash() {
1065        let id = StepId::from_string("step_hash");
1066        let mut set = HashSet::new();
1067        set.insert(id.clone());
1068        assert!(set.contains(&id));
1069    }
1070
1071    #[test]
1072    fn test_step_id_serde() {
1073        let id = StepId::from_string("step_serde");
1074        let json = serde_json::to_string(&id).unwrap();
1075        let parsed: StepId = serde_json::from_str(&json).unwrap();
1076        assert_eq!(id, parsed);
1077    }
1078
1079    // =========================================================================
1080    // GraphId Tests
1081    // =========================================================================
1082
1083    #[test]
1084    fn test_graph_id_new_has_correct_prefix() {
1085        let id = GraphId::new();
1086        assert!(id.as_str().starts_with("graph_"));
1087    }
1088
1089    #[test]
1090    fn test_graph_id_new_unique() {
1091        let id1 = GraphId::new();
1092        let id2 = GraphId::new();
1093        assert_ne!(id1, id2);
1094    }
1095
1096    #[test]
1097    fn test_graph_id_from_string() {
1098        let id = GraphId::from_string("graph_custom");
1099        assert_eq!(id.as_str(), "graph_custom");
1100    }
1101
1102    #[test]
1103    fn test_graph_id_display() {
1104        let id = GraphId::from_string("graph_display");
1105        assert_eq!(format!("{}", id), "graph_display");
1106    }
1107
1108    #[test]
1109    fn test_graph_id_default() {
1110        let id = GraphId::default();
1111        assert!(id.as_str().starts_with("graph_"));
1112    }
1113
1114    #[test]
1115    fn test_graph_id_serde() {
1116        let id = GraphId::from_string("graph_serde");
1117        let json = serde_json::to_string(&id).unwrap();
1118        let parsed: GraphId = serde_json::from_str(&json).unwrap();
1119        assert_eq!(id, parsed);
1120    }
1121
1122    // =========================================================================
1123    // ArtifactId Tests
1124    // =========================================================================
1125
1126    #[test]
1127    fn test_artifact_id_new_has_correct_prefix() {
1128        let id = ArtifactId::new();
1129        assert!(id.as_str().starts_with("artifact_"));
1130    }
1131
1132    #[test]
1133    fn test_artifact_id_new_unique() {
1134        let id1 = ArtifactId::new();
1135        let id2 = ArtifactId::new();
1136        assert_ne!(id1, id2);
1137    }
1138
1139    #[test]
1140    fn test_artifact_id_from_string() {
1141        let id = ArtifactId::from_string("artifact_custom");
1142        assert_eq!(id.as_str(), "artifact_custom");
1143    }
1144
1145    #[test]
1146    fn test_artifact_id_display() {
1147        let id = ArtifactId::from_string("artifact_display");
1148        assert_eq!(format!("{}", id), "artifact_display");
1149    }
1150
1151    #[test]
1152    fn test_artifact_id_default() {
1153        let id = ArtifactId::default();
1154        assert!(id.as_str().starts_with("artifact_"));
1155    }
1156
1157    #[test]
1158    fn test_artifact_id_serde() {
1159        let id = ArtifactId::from_string("artifact_serde");
1160        let json = serde_json::to_string(&id).unwrap();
1161        let parsed: ArtifactId = serde_json::from_str(&json).unwrap();
1162        assert_eq!(id, parsed);
1163    }
1164
1165    // =========================================================================
1166    // TenantId Tests
1167    // =========================================================================
1168
1169    #[test]
1170    fn test_tenant_id_new_has_correct_prefix() {
1171        let id = TenantId::new();
1172        assert!(id.as_str().starts_with("tenant_"));
1173    }
1174
1175    #[test]
1176    fn test_tenant_id_new_unique() {
1177        let id1 = TenantId::new();
1178        let id2 = TenantId::new();
1179        assert_ne!(id1, id2);
1180    }
1181
1182    #[test]
1183    fn test_tenant_id_from_string() {
1184        let id = TenantId::from_string("tenant_custom");
1185        assert_eq!(id.as_str(), "tenant_custom");
1186    }
1187
1188    #[test]
1189    fn test_tenant_id_display() {
1190        let id = TenantId::from_string("tenant_display");
1191        assert_eq!(format!("{}", id), "tenant_display");
1192    }
1193
1194    #[test]
1195    fn test_tenant_id_default() {
1196        let id = TenantId::default();
1197        assert!(id.as_str().starts_with("tenant_"));
1198    }
1199
1200    #[test]
1201    fn test_tenant_id_from_traits() {
1202        let id1: TenantId = String::from("tenant_string").into();
1203        let id2: TenantId = "tenant_str".into();
1204        assert_eq!(id1.as_str(), "tenant_string");
1205        assert_eq!(id2.as_str(), "tenant_str");
1206    }
1207
1208    #[test]
1209    fn test_tenant_id_serde() {
1210        let id = TenantId::from_string("tenant_serde");
1211        let json = serde_json::to_string(&id).unwrap();
1212        let parsed: TenantId = serde_json::from_str(&json).unwrap();
1213        assert_eq!(id, parsed);
1214    }
1215
1216    // =========================================================================
1217    // UserId Tests
1218    // =========================================================================
1219
1220    #[test]
1221    fn test_user_id_new_has_correct_prefix() {
1222        let id = UserId::new();
1223        assert!(id.as_str().starts_with("user_"));
1224    }
1225
1226    #[test]
1227    fn test_user_id_new_unique() {
1228        let id1 = UserId::new();
1229        let id2 = UserId::new();
1230        assert_ne!(id1, id2);
1231    }
1232
1233    #[test]
1234    fn test_user_id_from_string() {
1235        let id = UserId::from_string("user_custom");
1236        assert_eq!(id.as_str(), "user_custom");
1237    }
1238
1239    #[test]
1240    fn test_user_id_display() {
1241        let id = UserId::from_string("user_display");
1242        assert_eq!(format!("{}", id), "user_display");
1243    }
1244
1245    #[test]
1246    fn test_user_id_default() {
1247        let id = UserId::default();
1248        assert!(id.as_str().starts_with("user_"));
1249    }
1250
1251    #[test]
1252    fn test_user_id_from_traits() {
1253        let id1: UserId = String::from("user_string").into();
1254        let id2: UserId = "user_str".into();
1255        assert_eq!(id1.as_str(), "user_string");
1256        assert_eq!(id2.as_str(), "user_str");
1257    }
1258
1259    #[test]
1260    fn test_user_id_serde() {
1261        let id = UserId::from_string("user_serde");
1262        let json = serde_json::to_string(&id).unwrap();
1263        let parsed: UserId = serde_json::from_str(&json).unwrap();
1264        assert_eq!(id, parsed);
1265    }
1266
1267    // =========================================================================
1268    // StepType Tests
1269    // =========================================================================
1270
1271    #[test]
1272    fn test_step_type_display_llm_node() {
1273        assert_eq!(format!("{}", StepType::LlmNode), "LlmNode");
1274    }
1275
1276    #[test]
1277    fn test_step_type_display_graph_node() {
1278        assert_eq!(format!("{}", StepType::GraphNode), "GraphNode");
1279    }
1280
1281    #[test]
1282    fn test_step_type_display_tool_node() {
1283        assert_eq!(format!("{}", StepType::ToolNode), "ToolNode");
1284    }
1285
1286    #[test]
1287    fn test_step_type_display_function_node() {
1288        assert_eq!(format!("{}", StepType::FunctionNode), "FunctionNode");
1289    }
1290
1291    #[test]
1292    fn test_step_type_display_router_node() {
1293        assert_eq!(format!("{}", StepType::RouterNode), "RouterNode");
1294    }
1295
1296    #[test]
1297    fn test_step_type_display_branch_node() {
1298        assert_eq!(format!("{}", StepType::BranchNode), "BranchNode");
1299    }
1300
1301    #[test]
1302    fn test_step_type_display_loop_node() {
1303        assert_eq!(format!("{}", StepType::LoopNode), "LoopNode");
1304    }
1305
1306    #[test]
1307    fn test_step_type_serde_llm_node() {
1308        let step = StepType::LlmNode;
1309        let json = serde_json::to_string(&step).unwrap();
1310        assert_eq!(json, "\"LlmNode\"");
1311        let parsed: StepType = serde_json::from_str(&json).unwrap();
1312        assert_eq!(parsed, StepType::LlmNode);
1313    }
1314
1315    #[test]
1316    fn test_step_type_serde_all_variants() {
1317        let variants = vec![
1318            StepType::LlmNode,
1319            StepType::GraphNode,
1320            StepType::ToolNode,
1321            StepType::FunctionNode,
1322            StepType::RouterNode,
1323            StepType::BranchNode,
1324            StepType::LoopNode,
1325        ];
1326        for variant in variants {
1327            let json = serde_json::to_string(&variant).unwrap();
1328            let parsed: StepType = serde_json::from_str(&json).unwrap();
1329            assert_eq!(parsed, variant);
1330        }
1331    }
1332
1333    #[test]
1334    fn test_step_type_equality() {
1335        assert_eq!(StepType::LlmNode, StepType::LlmNode);
1336        assert_ne!(StepType::LlmNode, StepType::ToolNode);
1337    }
1338
1339    #[test]
1340    fn test_step_type_clone() {
1341        let step = StepType::GraphNode;
1342        let cloned = step.clone();
1343        assert_eq!(step, cloned);
1344    }
1345
1346    // =========================================================================
1347    // CallableType Tests (for billing/traceability)
1348    // =========================================================================
1349
1350    #[test]
1351    fn test_callable_type_display_all() {
1352        assert_eq!(format!("{}", CallableType::Completion), "completion");
1353        assert_eq!(format!("{}", CallableType::Chat), "chat");
1354        assert_eq!(format!("{}", CallableType::Agent), "agent");
1355        assert_eq!(format!("{}", CallableType::Workflow), "workflow");
1356        assert_eq!(format!("{}", CallableType::Background), "background");
1357        assert_eq!(format!("{}", CallableType::Composite), "composite");
1358        assert_eq!(format!("{}", CallableType::Tool), "tool");
1359        assert_eq!(format!("{}", CallableType::Custom), "custom");
1360    }
1361
1362    #[test]
1363    fn test_callable_type_default() {
1364        let default_type = CallableType::default();
1365        assert_eq!(default_type, CallableType::Agent);
1366    }
1367
1368    #[test]
1369    fn test_callable_type_serde_all_variants() {
1370        let variants = vec![
1371            CallableType::Completion,
1372            CallableType::Chat,
1373            CallableType::Agent,
1374            CallableType::Workflow,
1375            CallableType::Background,
1376            CallableType::Composite,
1377            CallableType::Tool,
1378            CallableType::Custom,
1379        ];
1380        for variant in variants {
1381            let json = serde_json::to_string(&variant).unwrap();
1382            let parsed: CallableType = serde_json::from_str(&json).unwrap();
1383            assert_eq!(parsed, variant);
1384        }
1385    }
1386
1387    #[test]
1388    fn test_callable_type_serde_snake_case() {
1389        // Verify snake_case serialization
1390        let json = serde_json::to_string(&CallableType::Background).unwrap();
1391        assert_eq!(json, "\"background\"");
1392
1393        let json = serde_json::to_string(&CallableType::Workflow).unwrap();
1394        assert_eq!(json, "\"workflow\"");
1395    }
1396
1397    #[test]
1398    fn test_callable_type_equality() {
1399        assert_eq!(CallableType::Agent, CallableType::Agent);
1400        assert_ne!(CallableType::Agent, CallableType::Chat);
1401    }
1402
1403    #[test]
1404    fn test_callable_type_clone() {
1405        let callable = CallableType::Workflow;
1406        let cloned = callable.clone();
1407        assert_eq!(callable, cloned);
1408    }
1409
1410    #[test]
1411    fn test_callable_type_hash() {
1412        use std::collections::HashSet;
1413        let mut set = HashSet::new();
1414        set.insert(CallableType::Agent);
1415        set.insert(CallableType::Chat);
1416        set.insert(CallableType::Agent); // duplicate
1417        assert_eq!(set.len(), 2);
1418    }
1419
1420    // =========================================================================
1421    // ParentType Tests
1422    // =========================================================================
1423
1424    #[test]
1425    fn test_parent_type_display_user_message() {
1426        assert_eq!(format!("{}", ParentType::UserMessage), "user_message");
1427    }
1428
1429    #[test]
1430    fn test_parent_type_display_schedule_event() {
1431        assert_eq!(format!("{}", ParentType::ScheduleEvent), "schedule_event");
1432    }
1433
1434    #[test]
1435    fn test_parent_type_display_step_execution() {
1436        assert_eq!(format!("{}", ParentType::StepExecution), "step_execution");
1437    }
1438
1439    #[test]
1440    fn test_parent_type_display_webhook() {
1441        assert_eq!(format!("{}", ParentType::Webhook), "webhook");
1442    }
1443
1444    #[test]
1445    fn test_parent_type_display_a2a_request() {
1446        assert_eq!(format!("{}", ParentType::A2aRequest), "a2a_request");
1447    }
1448
1449    #[test]
1450    fn test_parent_type_display_system() {
1451        assert_eq!(format!("{}", ParentType::System), "system");
1452    }
1453
1454    #[test]
1455    fn test_parent_type_serde_all_variants() {
1456        let variants = vec![
1457            ParentType::UserMessage,
1458            ParentType::ScheduleEvent,
1459            ParentType::StepExecution,
1460            ParentType::Webhook,
1461            ParentType::A2aRequest,
1462            ParentType::System,
1463        ];
1464        for variant in variants {
1465            let json = serde_json::to_string(&variant).unwrap();
1466            let parsed: ParentType = serde_json::from_str(&json).unwrap();
1467            assert_eq!(parsed, variant);
1468        }
1469    }
1470
1471    #[test]
1472    fn test_parent_type_equality() {
1473        assert_eq!(ParentType::System, ParentType::System);
1474        assert_ne!(ParentType::System, ParentType::Webhook);
1475    }
1476
1477    // =========================================================================
1478    // ParentLink Tests
1479    // =========================================================================
1480
1481    #[test]
1482    fn test_parent_link_new() {
1483        let link = ParentLink::new("msg_123", ParentType::UserMessage);
1484        assert_eq!(link.parent_id, "msg_123");
1485        assert_eq!(link.parent_type, ParentType::UserMessage);
1486    }
1487
1488    #[test]
1489    fn test_parent_link_new_with_string() {
1490        let link = ParentLink::new(String::from("wh_456"), ParentType::Webhook);
1491        assert_eq!(link.parent_id, "wh_456");
1492        assert_eq!(link.parent_type, ParentType::Webhook);
1493    }
1494
1495    #[test]
1496    fn test_parent_link_from_user_message() {
1497        let link = ParentLink::from_user_message("msg_user_789");
1498        assert_eq!(link.parent_id, "msg_user_789");
1499        assert_eq!(link.parent_type, ParentType::UserMessage);
1500    }
1501
1502    #[test]
1503    fn test_parent_link_from_step() {
1504        let step_id = StepId::from_string("step_abc123");
1505        let link = ParentLink::from_step(&step_id);
1506        assert_eq!(link.parent_id, "step_abc123");
1507        assert_eq!(link.parent_type, ParentType::StepExecution);
1508    }
1509
1510    #[test]
1511    fn test_parent_link_execution() {
1512        let exec_id = ExecutionId::from_string("exec_parent");
1513        let link = ParentLink::execution(exec_id);
1514        assert_eq!(link.parent_id, "exec_parent");
1515        assert_eq!(link.parent_type, ParentType::StepExecution);
1516    }
1517
1518    #[test]
1519    fn test_parent_link_system() {
1520        let link = ParentLink::system();
1521        assert_eq!(link.parent_id, "system");
1522        assert_eq!(link.parent_type, ParentType::System);
1523    }
1524
1525    #[test]
1526    fn test_parent_link_serde() {
1527        let link = ParentLink::new("test_id", ParentType::A2aRequest);
1528        let json = serde_json::to_string(&link).unwrap();
1529        let parsed: ParentLink = serde_json::from_str(&json).unwrap();
1530        assert_eq!(link, parsed);
1531    }
1532
1533    #[test]
1534    fn test_parent_link_equality() {
1535        let link1 = ParentLink::new("same_id", ParentType::System);
1536        let link2 = ParentLink::new("same_id", ParentType::System);
1537        let link3 = ParentLink::new("different_id", ParentType::System);
1538        assert_eq!(link1, link2);
1539        assert_ne!(link1, link3);
1540    }
1541
1542    #[test]
1543    fn test_parent_link_clone() {
1544        let link = ParentLink::from_user_message("msg_clone");
1545        let cloned = link.clone();
1546        assert_eq!(link, cloned);
1547    }
1548
1549    // =========================================================================
1550    // Prefixes Tests
1551    // =========================================================================
1552
1553    #[test]
1554    fn test_prefix_execution() {
1555        assert_eq!(prefixes::EXECUTION, "exec_");
1556    }
1557
1558    #[test]
1559    fn test_prefix_step() {
1560        assert_eq!(prefixes::STEP, "step_");
1561    }
1562
1563    #[test]
1564    fn test_prefix_artifact() {
1565        assert_eq!(prefixes::ARTIFACT, "artifact_");
1566    }
1567
1568    #[test]
1569    fn test_prefix_graph() {
1570        assert_eq!(prefixes::GRAPH, "graph_");
1571    }
1572
1573    #[test]
1574    fn test_prefix_tenant() {
1575        assert_eq!(prefixes::TENANT, "tenant_");
1576    }
1577
1578    #[test]
1579    fn test_prefix_user() {
1580        assert_eq!(prefixes::USER, "user_");
1581    }
1582
1583    #[test]
1584    fn test_prefix_event() {
1585        assert_eq!(prefixes::EVENT, "evt_");
1586    }
1587
1588    #[test]
1589    fn test_prefix_decision() {
1590        assert_eq!(prefixes::DECISION, "dec_");
1591    }
1592
1593    #[test]
1594    fn test_prefix_control() {
1595        assert_eq!(prefixes::CONTROL, "ctrl_");
1596    }
1597
1598    // =========================================================================
1599    // Type Alias Tests
1600    // =========================================================================
1601
1602    #[test]
1603    fn test_run_id_alias() {
1604        let exec_id = ExecutionId::from_string("exec_alias");
1605        let run_id: RunId = exec_id.clone();
1606        assert_eq!(exec_id.as_str(), run_id.as_str());
1607    }
1608
1609    #[test]
1610    fn test_node_id_alias() {
1611        let step_id = StepId::from_string("step_alias");
1612        let node_id: NodeId = step_id.clone();
1613        assert_eq!(step_id.as_str(), node_id.as_str());
1614    }
1615
1616    // =========================================================================
1617    // KSUID Format Tests
1618    // =========================================================================
1619
1620    #[test]
1621    fn test_execution_id_ksuid_length() {
1622        let id = ExecutionId::new();
1623        // exec_ (5 chars) + KSUID (27 chars) = 32 chars
1624        assert_eq!(id.as_str().len(), 32);
1625    }
1626
1627    #[test]
1628    fn test_step_id_ksuid_length() {
1629        let id = StepId::new();
1630        // step_ (5 chars) + KSUID (27 chars) = 32 chars
1631        assert_eq!(id.as_str().len(), 32);
1632    }
1633
1634    #[test]
1635    fn test_graph_id_ksuid_length() {
1636        let id = GraphId::new();
1637        // graph_ (6 chars) + KSUID (27 chars) = 33 chars
1638        assert_eq!(id.as_str().len(), 33);
1639    }
1640
1641    #[test]
1642    fn test_artifact_id_ksuid_length() {
1643        let id = ArtifactId::new();
1644        // artifact_ (9 chars) + KSUID (27 chars) = 36 chars
1645        assert_eq!(id.as_str().len(), 36);
1646    }
1647
1648    #[test]
1649    fn test_tenant_id_ksuid_length() {
1650        let id = TenantId::new();
1651        // tenant_ (7 chars) + KSUID (27 chars) = 34 chars
1652        assert_eq!(id.as_str().len(), 34);
1653    }
1654
1655    #[test]
1656    fn test_user_id_ksuid_length() {
1657        let id = UserId::new();
1658        // user_ (5 chars) + KSUID (27 chars) = 32 chars
1659        assert_eq!(id.as_str().len(), 32);
1660    }
1661
1662    // =========================================================================
1663    // Sortability Tests (KSUIDs are time-sorted)
1664    // =========================================================================
1665
1666    #[test]
1667    fn test_execution_ids_sortable() {
1668        // Generate multiple IDs quickly and verify they're all unique
1669        let mut ids: Vec<ExecutionId> = (0..10).map(|_| ExecutionId::new()).collect();
1670        let original_count = ids.len();
1671
1672        // Deduplicate to check uniqueness
1673        ids.sort_by(|a, b| a.as_str().cmp(b.as_str()));
1674        ids.dedup_by(|a, b| a.as_str() == b.as_str());
1675        assert_eq!(
1676            ids.len(),
1677            original_count,
1678            "All generated IDs should be unique"
1679        );
1680    }
1681
1682    #[test]
1683    fn test_ksuid_contains_alphanumeric() {
1684        let id = ExecutionId::new();
1685        let ksuid_part = &id.as_str()[5..]; // After "exec_"
1686        assert!(ksuid_part.chars().all(|c| c.is_ascii_alphanumeric()));
1687    }
1688
1689    // =========================================================================
1690    // ThreadId Tests
1691    // =========================================================================
1692
1693    #[test]
1694    fn test_thread_id_new_has_correct_prefix() {
1695        let id = ThreadId::new();
1696        assert!(
1697            id.as_str().starts_with("thread_"),
1698            "ThreadId should start with 'thread_'"
1699        );
1700    }
1701
1702    #[test]
1703    fn test_thread_id_new_unique() {
1704        let id1 = ThreadId::new();
1705        let id2 = ThreadId::new();
1706        assert_ne!(id1, id2, "Each new ThreadId should be unique");
1707    }
1708
1709    #[test]
1710    fn test_thread_id_from_string() {
1711        let id = ThreadId::from_string("thread_custom123");
1712        assert_eq!(id.as_str(), "thread_custom123");
1713    }
1714
1715    #[test]
1716    fn test_thread_id_display() {
1717        let id = ThreadId::from_string("thread_display_test");
1718        assert_eq!(format!("{}", id), "thread_display_test");
1719    }
1720
1721    #[test]
1722    fn test_thread_id_default() {
1723        let id = ThreadId::default();
1724        assert!(id.as_str().starts_with("thread_"));
1725    }
1726
1727    #[test]
1728    fn test_thread_id_from_traits() {
1729        let id1: ThreadId = String::from("thread_string").into();
1730        let id2: ThreadId = "thread_str".into();
1731        assert_eq!(id1.as_str(), "thread_string");
1732        assert_eq!(id2.as_str(), "thread_str");
1733    }
1734
1735    #[test]
1736    fn test_thread_id_hash() {
1737        let id = ThreadId::from_string("thread_hash");
1738        let mut set = HashSet::new();
1739        set.insert(id.clone());
1740        assert!(set.contains(&id));
1741    }
1742
1743    #[test]
1744    fn test_thread_id_serde() {
1745        let id = ThreadId::from_string("thread_serde");
1746        let json = serde_json::to_string(&id).unwrap();
1747        let parsed: ThreadId = serde_json::from_str(&json).unwrap();
1748        assert_eq!(id, parsed);
1749    }
1750
1751    #[test]
1752    fn test_thread_id_ksuid_length() {
1753        let id = ThreadId::new();
1754        // thread_ (7 chars) + KSUID (27 chars) = 34 chars
1755        assert_eq!(id.as_str().len(), 34);
1756    }
1757
1758    // =========================================================================
1759    // MessageId Tests
1760    // =========================================================================
1761
1762    #[test]
1763    fn test_message_id_new_has_correct_prefix() {
1764        let id = MessageId::new();
1765        assert!(
1766            id.as_str().starts_with("msg_"),
1767            "MessageId should start with 'msg_'"
1768        );
1769    }
1770
1771    #[test]
1772    fn test_message_id_new_unique() {
1773        let id1 = MessageId::new();
1774        let id2 = MessageId::new();
1775        assert_ne!(id1, id2, "Each new MessageId should be unique");
1776    }
1777
1778    #[test]
1779    fn test_message_id_from_string() {
1780        let id = MessageId::from_string("msg_custom123");
1781        assert_eq!(id.as_str(), "msg_custom123");
1782    }
1783
1784    #[test]
1785    fn test_message_id_display() {
1786        let id = MessageId::from_string("msg_display_test");
1787        assert_eq!(format!("{}", id), "msg_display_test");
1788    }
1789
1790    #[test]
1791    fn test_message_id_default() {
1792        let id = MessageId::default();
1793        assert!(id.as_str().starts_with("msg_"));
1794    }
1795
1796    #[test]
1797    fn test_message_id_from_traits() {
1798        let id1: MessageId = String::from("msg_string").into();
1799        let id2: MessageId = "msg_str".into();
1800        assert_eq!(id1.as_str(), "msg_string");
1801        assert_eq!(id2.as_str(), "msg_str");
1802    }
1803
1804    #[test]
1805    fn test_message_id_hash() {
1806        let id = MessageId::from_string("msg_hash");
1807        let mut set = HashSet::new();
1808        set.insert(id.clone());
1809        assert!(set.contains(&id));
1810    }
1811
1812    #[test]
1813    fn test_message_id_serde() {
1814        let id = MessageId::from_string("msg_serde");
1815        let json = serde_json::to_string(&id).unwrap();
1816        let parsed: MessageId = serde_json::from_str(&json).unwrap();
1817        assert_eq!(id, parsed);
1818    }
1819
1820    #[test]
1821    fn test_message_id_ksuid_length() {
1822        let id = MessageId::new();
1823        // msg_ (4 chars) + KSUID (27 chars) = 31 chars
1824        assert_eq!(id.as_str().len(), 31);
1825    }
1826
1827    // =========================================================================
1828    // Prefix Tests for new IDs
1829    // =========================================================================
1830
1831    #[test]
1832    fn test_prefix_thread() {
1833        assert_eq!(prefixes::THREAD, "thread_");
1834    }
1835
1836    #[test]
1837    fn test_prefix_message() {
1838        assert_eq!(prefixes::MESSAGE, "msg_");
1839    }
1840
1841    // =========================================================================
1842    // SpawnMode Tests
1843    // =========================================================================
1844
1845    #[test]
1846    fn test_spawn_mode_inline_default() {
1847        // Inline is not the default since we use Option<SpawnMode>
1848        // but we can still test the variant
1849        let mode = SpawnMode::Inline;
1850        assert_eq!(mode, SpawnMode::Inline);
1851    }
1852
1853    #[test]
1854    fn test_spawn_mode_child_default_fields() {
1855        let mode = SpawnMode::Child {
1856            background: false,
1857            inherit_inbox: false,
1858            policies: None,
1859        };
1860        if let SpawnMode::Child {
1861            background,
1862            inherit_inbox,
1863            policies,
1864        } = mode
1865        {
1866            assert!(!background, "default background should be false");
1867            assert!(!inherit_inbox, "default inherit_inbox should be false");
1868            assert!(policies.is_none(), "default policies should be None");
1869        } else {
1870            panic!("Expected SpawnMode::Child");
1871        }
1872    }
1873
1874    #[test]
1875    fn test_spawn_mode_child_with_inherit_inbox() {
1876        let mode = SpawnMode::Child {
1877            background: false,
1878            inherit_inbox: true,
1879            policies: None,
1880        };
1881        if let SpawnMode::Child { inherit_inbox, .. } = mode {
1882            assert!(inherit_inbox, "inherit_inbox should be true");
1883        } else {
1884            panic!("Expected SpawnMode::Child");
1885        }
1886    }
1887
1888    #[test]
1889    fn test_spawn_mode_child_background() {
1890        let mode = SpawnMode::Child {
1891            background: true,
1892            inherit_inbox: false,
1893            policies: None,
1894        };
1895        if let SpawnMode::Child { background, .. } = mode {
1896            assert!(background, "background should be true");
1897        } else {
1898            panic!("Expected SpawnMode::Child");
1899        }
1900    }
1901
1902    #[test]
1903    fn test_spawn_mode_serde_inline() {
1904        let mode = SpawnMode::Inline;
1905        let json = serde_json::to_string(&mode).unwrap();
1906        assert!(
1907            json.contains("\"mode\":\"inline\""),
1908            "Inline should serialize with mode tag"
1909        );
1910        let parsed: SpawnMode = serde_json::from_str(&json).unwrap();
1911        assert_eq!(mode, parsed);
1912    }
1913
1914    #[test]
1915    fn test_spawn_mode_serde_child() {
1916        let mode = SpawnMode::Child {
1917            background: true,
1918            inherit_inbox: true,
1919            policies: None,
1920        };
1921        let json = serde_json::to_string(&mode).unwrap();
1922        assert!(
1923            json.contains("\"mode\":\"child\""),
1924            "Child should serialize with mode tag"
1925        );
1926        assert!(
1927            json.contains("\"background\":true"),
1928            "background should be serialized"
1929        );
1930        assert!(
1931            json.contains("\"inherit_inbox\":true"),
1932            "inherit_inbox should be serialized"
1933        );
1934        let parsed: SpawnMode = serde_json::from_str(&json).unwrap();
1935        assert_eq!(mode, parsed);
1936    }
1937
1938    #[test]
1939    fn test_spawn_mode_serde_child_with_policies() {
1940        let policies = serde_json::json!({
1941            "max_steps": 100,
1942            "max_tokens": 10000
1943        });
1944        let mode = SpawnMode::Child {
1945            background: false,
1946            inherit_inbox: true,
1947            policies: Some(policies.clone()),
1948        };
1949        let json = serde_json::to_string(&mode).unwrap();
1950        let parsed: SpawnMode = serde_json::from_str(&json).unwrap();
1951        assert_eq!(mode, parsed);
1952        if let SpawnMode::Child {
1953            policies: Some(p), ..
1954        } = parsed
1955        {
1956            assert_eq!(p["max_steps"], 100);
1957        } else {
1958            panic!("Expected SpawnMode::Child with policies");
1959        }
1960    }
1961
1962    #[test]
1963    fn test_spawn_mode_equality() {
1964        let mode1 = SpawnMode::Inline;
1965        let mode2 = SpawnMode::Inline;
1966        let mode3 = SpawnMode::Child {
1967            background: false,
1968            inherit_inbox: false,
1969            policies: None,
1970        };
1971        assert_eq!(mode1, mode2);
1972        assert_ne!(mode1, mode3);
1973    }
1974
1975    #[test]
1976    fn test_spawn_mode_clone() {
1977        let mode = SpawnMode::Child {
1978            background: true,
1979            inherit_inbox: true,
1980            policies: Some(serde_json::json!({"key": "value"})),
1981        };
1982        let cloned = mode.clone();
1983        assert_eq!(mode, cloned);
1984    }
1985
1986    // =========================================================================
1987    // CancellationPolicy Tests
1988    // =========================================================================
1989
1990    #[test]
1991    fn test_cancellation_policy_default() {
1992        let policy = CancellationPolicy::default();
1993        assert_eq!(policy, CancellationPolicy::CascadeCancel);
1994    }
1995
1996    #[test]
1997    fn test_cancellation_policy_variants() {
1998        let cascade = CancellationPolicy::CascadeCancel;
1999        let wait = CancellationPolicy::WaitForChildren;
2000        let detach = CancellationPolicy::Detach;
2001
2002        assert_ne!(cascade, wait);
2003        assert_ne!(wait, detach);
2004        assert_ne!(cascade, detach);
2005    }
2006
2007    #[test]
2008    fn test_cancellation_policy_serde_cascade() {
2009        let policy = CancellationPolicy::CascadeCancel;
2010        let json = serde_json::to_string(&policy).unwrap();
2011        assert_eq!(json, "\"cascade_cancel\"");
2012        let parsed: CancellationPolicy = serde_json::from_str(&json).unwrap();
2013        assert_eq!(policy, parsed);
2014    }
2015
2016    #[test]
2017    fn test_cancellation_policy_serde_wait() {
2018        let policy = CancellationPolicy::WaitForChildren;
2019        let json = serde_json::to_string(&policy).unwrap();
2020        assert_eq!(json, "\"wait_for_children\"");
2021        let parsed: CancellationPolicy = serde_json::from_str(&json).unwrap();
2022        assert_eq!(policy, parsed);
2023    }
2024
2025    #[test]
2026    fn test_cancellation_policy_serde_detach() {
2027        let policy = CancellationPolicy::Detach;
2028        let json = serde_json::to_string(&policy).unwrap();
2029        assert_eq!(json, "\"detach\"");
2030        let parsed: CancellationPolicy = serde_json::from_str(&json).unwrap();
2031        assert_eq!(policy, parsed);
2032    }
2033
2034    #[test]
2035    fn test_cancellation_policy_all_variants_serde() {
2036        let variants = vec![
2037            CancellationPolicy::CascadeCancel,
2038            CancellationPolicy::WaitForChildren,
2039            CancellationPolicy::Detach,
2040        ];
2041        for variant in variants {
2042            let json = serde_json::to_string(&variant).unwrap();
2043            let parsed: CancellationPolicy = serde_json::from_str(&json).unwrap();
2044            assert_eq!(parsed, variant);
2045        }
2046    }
2047
2048    #[test]
2049    fn test_cancellation_policy_clone() {
2050        let policy = CancellationPolicy::WaitForChildren;
2051        let cloned = policy.clone();
2052        assert_eq!(policy, cloned);
2053    }
2054
2055    #[test]
2056    fn test_cancellation_policy_hash() {
2057        let policy = CancellationPolicy::Detach;
2058        let mut set = HashSet::new();
2059        set.insert(policy.clone());
2060        assert!(set.contains(&policy));
2061    }
2062
2063    // =========================================================================
2064    // StepSource with SpawnMode Tests
2065    // =========================================================================
2066
2067    #[test]
2068    fn test_step_source_with_spawn_mode() {
2069        let source = StepSource {
2070            source_type: StepSourceType::Discovered,
2071            triggered_by: Some("step_parent".to_string()),
2072            reason: Some("test spawn".to_string()),
2073            depth: Some(1),
2074            spawn_mode: Some(SpawnMode::Child {
2075                background: false,
2076                inherit_inbox: true,
2077                policies: None,
2078            }),
2079        };
2080        assert!(source.spawn_mode.is_some());
2081        if let Some(SpawnMode::Child { inherit_inbox, .. }) = source.spawn_mode {
2082            assert!(inherit_inbox);
2083        }
2084    }
2085
2086    #[test]
2087    fn test_step_source_discovered_default_spawn_mode() {
2088        let spawner = StepId::new();
2089        let source = StepSource::discovered(&spawner, "test reason", 1);
2090        assert!(
2091            source.spawn_mode.is_none(),
2092            "discovered() should default to None spawn_mode"
2093        );
2094    }
2095
2096    #[test]
2097    fn test_step_source_serde_with_spawn_mode() {
2098        let source = StepSource {
2099            source_type: StepSourceType::Discovered,
2100            triggered_by: Some("step_123".to_string()),
2101            reason: Some("test reason".to_string()),
2102            depth: Some(2),
2103            spawn_mode: Some(SpawnMode::Inline),
2104        };
2105        let json = serde_json::to_string(&source).unwrap();
2106        let parsed: StepSource = serde_json::from_str(&json).unwrap();
2107        assert_eq!(source, parsed);
2108    }
2109}