Skip to main content

enact_core/kernel/
event.rs

1//! Execution Events for telemetry and audit
2//!
3//! This module defines all event types emitted during graph execution.
4//! Events provide observability into execution progress, decisions,
5//! and control signals.
6//!
7//! ## Event Types
8//! - `ExecutionEvent` - Base event for all execution events
9//! - `DecisionRecord` - Audit trail for decisions
10//! - `ControlEvent` - Governance and intervention signals
11//!
12//! @see docs/TECHNICAL/01-EXECUTION-TELEMETRY.md
13
14use super::ids::{ArtifactId, ExecutionId, ParentLink, StepId, StepType, TenantId, UserId};
15use serde::{Deserialize, Serialize};
16use svix_ksuid::{Ksuid, KsuidLike};
17
18/// Generate a new event ID
19fn new_event_id() -> String {
20    format!("evt_{}", Ksuid::new(None, None))
21}
22
23// =============================================================================
24// Event Types
25// =============================================================================
26
27/// ExecutionEventType - All event types in the execution lifecycle
28///
29/// Naming convention: `<entity>.<action>`
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(rename_all = "snake_case")]
32pub enum ExecutionEventType {
33    // Execution lifecycle
34    ExecutionStart,
35    ExecutionEnd,
36    ExecutionFailed,
37    ExecutionCancelled,
38
39    // Step lifecycle
40    StepStart,
41    StepEnd,
42    StepFailed,
43    StepDiscovered,
44
45    // Artifact events
46    ArtifactCreated,
47
48    // State snapshots
49    StateSnapshot,
50
51    // Decision audit
52    DecisionMade,
53
54    // Control signals
55    ControlPause,
56    ControlResume,
57    ControlCancel,
58
59    // Inbox messages (INV-INBOX-003: audit trail)
60    InboxMessage,
61
62    // Tool execution
63    ToolCallStart,
64    ToolCallEnd,
65
66    // Agentic loop events
67    CheckpointSaved,
68    GoalEvaluated,
69
70    // LLM Call Observability
71    LlmCallStart,
72    LlmCallEnd,
73    LlmCallFailed,
74
75    // Token & Cost Accounting
76    TokenUsageRecorded,
77
78    // Memory Access
79    MemoryRecalled,
80    MemoryStored,
81
82    // Guardrail/Safety
83    GuardrailEvaluated,
84
85    // Reasoning & Context
86    ReasoningTrace,
87    ContextSnapshot,
88
89    // Feedback
90    FeedbackReceived,
91}
92
93impl ExecutionEventType {
94    /// Get the event type as a string (e.g., "execution.start")
95    pub fn as_str(&self) -> &'static str {
96        match self {
97            Self::ExecutionStart => "execution.start",
98            Self::ExecutionEnd => "execution.end",
99            Self::ExecutionFailed => "execution.failed",
100            Self::ExecutionCancelled => "execution.cancelled",
101            Self::StepStart => "step.start",
102            Self::StepEnd => "step.end",
103            Self::StepFailed => "step.failed",
104            Self::StepDiscovered => "step.discovered",
105            Self::ArtifactCreated => "artifact.created",
106            Self::StateSnapshot => "state.snapshot",
107            Self::DecisionMade => "decision.made",
108            Self::ControlPause => "control.pause",
109            Self::ControlResume => "control.resume",
110            Self::ControlCancel => "control.cancel",
111            Self::InboxMessage => "inbox.message",
112            Self::ToolCallStart => "tool.start",
113            Self::ToolCallEnd => "tool.end",
114            Self::CheckpointSaved => "state.checkpoint",
115            Self::GoalEvaluated => "goal.evaluated",
116            Self::LlmCallStart => "llm.call.start",
117            Self::LlmCallEnd => "llm.call.end",
118            Self::LlmCallFailed => "llm.call.failed",
119            Self::TokenUsageRecorded => "token.usage",
120            Self::MemoryRecalled => "memory.recalled",
121            Self::MemoryStored => "memory.stored",
122            Self::GuardrailEvaluated => "guardrail.evaluated",
123            Self::ReasoningTrace => "reasoning.trace",
124            Self::ContextSnapshot => "context.snapshot",
125            Self::FeedbackReceived => "feedback.received",
126        }
127    }
128}
129
130// =============================================================================
131// Execution Context (attached to all events)
132// =============================================================================
133
134/// ExecutionContext - Minimal context attached to every event
135///
136/// Replaces the old BaseIdHierarchy with a simpler structure.
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct ExecutionContext {
139    /// Required: The execution this event belongs to
140    pub execution_id: ExecutionId,
141
142    /// Optional: The specific step
143    pub step_id: Option<StepId>,
144
145    /// Optional: The artifact produced
146    pub artifact_id: Option<ArtifactId>,
147
148    /// Parent linkage (causal origin)
149    pub parent: Option<ParentLink>,
150
151    /// Tenant context
152    pub tenant_id: Option<TenantId>,
153    pub user_id: Option<UserId>,
154}
155
156impl ExecutionContext {
157    /// Create a new ExecutionContext for an execution
158    pub fn new(execution_id: ExecutionId) -> Self {
159        Self {
160            execution_id,
161            step_id: None,
162            artifact_id: None,
163            parent: None,
164            tenant_id: None,
165            user_id: None,
166        }
167    }
168
169    /// Add step context
170    pub fn with_step(mut self, step_id: StepId) -> Self {
171        self.step_id = Some(step_id);
172        self
173    }
174
175    /// Add artifact context
176    pub fn with_artifact(mut self, artifact_id: ArtifactId) -> Self {
177        self.artifact_id = Some(artifact_id);
178        self
179    }
180
181    /// Add parent linkage
182    pub fn with_parent(mut self, parent: ParentLink) -> Self {
183        self.parent = Some(parent);
184        self
185    }
186
187    /// Add tenant context
188    pub fn with_tenant(mut self, tenant_id: TenantId, user_id: Option<UserId>) -> Self {
189        self.tenant_id = Some(tenant_id);
190        self.user_id = user_id;
191        self
192    }
193}
194
195// =============================================================================
196// Execution Event (Base Event Type)
197// =============================================================================
198
199/// ExecutionEvent - Base event schema for all execution events
200///
201/// All events include execution context for traceability.
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct ExecutionEvent {
204    /// Unique event ID
205    pub event_id: String,
206
207    /// Event type
208    pub event_type: ExecutionEventType,
209
210    /// Execution context
211    pub context: ExecutionContext,
212
213    /// Timestamp
214    pub timestamp: chrono::DateTime<chrono::Utc>,
215
216    /// Duration in milliseconds (for start/end pairs)
217    pub duration_ms: Option<u64>,
218
219    /// Event-specific payload
220    pub payload: Option<serde_json::Value>,
221}
222
223impl ExecutionEvent {
224    /// Create a new ExecutionEvent
225    pub fn new(event_type: ExecutionEventType, context: ExecutionContext) -> Self {
226        Self {
227            event_id: new_event_id(),
228            event_type,
229            context,
230            timestamp: chrono::Utc::now(),
231            duration_ms: None,
232            payload: None,
233        }
234    }
235
236    /// Add duration
237    pub fn with_duration(mut self, ms: u64) -> Self {
238        self.duration_ms = Some(ms);
239        self
240    }
241
242    /// Add payload
243    pub fn with_payload(mut self, payload: serde_json::Value) -> Self {
244        self.payload = Some(payload);
245        self
246    }
247
248    // --- Factory methods for common events ---
249
250    /// Create an execution.start event
251    pub fn execution_start(execution_id: ExecutionId, parent: Option<ParentLink>) -> Self {
252        let mut ctx = ExecutionContext::new(execution_id);
253        if let Some(p) = parent {
254            ctx = ctx.with_parent(p);
255        }
256        Self::new(ExecutionEventType::ExecutionStart, ctx)
257    }
258
259    /// Create an execution.end event
260    pub fn execution_end(execution_id: ExecutionId, duration_ms: Option<u64>) -> Self {
261        let ctx = ExecutionContext::new(execution_id);
262        let mut event = Self::new(ExecutionEventType::ExecutionEnd, ctx);
263        event.duration_ms = duration_ms;
264        event
265    }
266
267    /// Create a step.start event
268    pub fn step_start(
269        execution_id: ExecutionId,
270        step_id: StepId,
271        step_type: StepType,
272        name: &str,
273    ) -> Self {
274        let ctx = ExecutionContext::new(execution_id).with_step(step_id);
275        Self::new(ExecutionEventType::StepStart, ctx).with_payload(serde_json::json!({
276            "step_type": step_type.to_string(),
277            "name": name,
278        }))
279    }
280
281    /// Create a step.end event
282    pub fn step_end(execution_id: ExecutionId, step_id: StepId, duration_ms: u64) -> Self {
283        let ctx = ExecutionContext::new(execution_id).with_step(step_id);
284        Self::new(ExecutionEventType::StepEnd, ctx).with_duration(duration_ms)
285    }
286
287    /// Create an artifact.created event
288    pub fn artifact_created(
289        execution_id: ExecutionId,
290        step_id: StepId,
291        artifact_id: ArtifactId,
292        artifact_type: &str,
293    ) -> Self {
294        let ctx = ExecutionContext::new(execution_id)
295            .with_step(step_id)
296            .with_artifact(artifact_id);
297        Self::new(ExecutionEventType::ArtifactCreated, ctx).with_payload(serde_json::json!({
298            "artifact_type": artifact_type,
299        }))
300    }
301}
302
303// =============================================================================
304// Decision Record (Audit Trail)
305// =============================================================================
306
307/// DecisionType - Types of decisions made during execution
308///
309/// @see packages/enact-schemas/src/streaming.schemas.ts - decisionEventDataSchema.type
310#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
311#[serde(rename_all = "snake_case")]
312pub enum DecisionType {
313    /// Route selection (which agent/node to call)
314    Routing,
315    /// Conditional branch decision
316    Branch,
317    /// HITL approval decision
318    Approval,
319    /// Escalate to human
320    Escalation,
321    /// Retry failed operation
322    Retry,
323    /// Use fallback strategy
324    Fallback,
325    /// Policy evaluation decision
326    PolicyEvaluation,
327    /// Tool selection decision
328    ToolSelection,
329    /// Rejection decision
330    Rejection,
331}
332
333/// DecisionInput - Inputs that informed the decision
334#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct DecisionInput {
336    /// Facts used in decision
337    pub facts: Vec<String>,
338    /// Constraints applied
339    pub constraints: Vec<String>,
340    /// Evidence IDs (artifacts, memory, RAG results)
341    pub evidence_ids: Vec<String>,
342}
343
344/// DecisionAlternative - An alternative option that was not chosen
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct DecisionAlternative {
347    /// Name/ID of the alternative
348    pub option: String,
349    /// Confidence score (0-1)
350    pub score: f64,
351    /// Why it was rejected
352    pub rejected_reason: String,
353}
354
355/// ModelContext - Model configuration used for decision
356#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct ModelContext {
358    /// Provider (e.g., 'openai', 'anthropic')
359    pub provider: String,
360    /// Model (e.g., 'gpt-4', 'claude-3-opus')
361    pub model: String,
362    /// Model version
363    pub version: Option<String>,
364    /// Temperature
365    pub temperature: Option<f64>,
366    /// Max tokens
367    pub max_tokens: Option<u32>,
368}
369
370/// DecisionRecord - Audit trail for decisions made during execution
371///
372/// Records what decision was made, why, what alternatives were considered,
373/// and what context/model was used.
374#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct DecisionRecord {
376    /// Unique decision ID
377    pub decision_id: String,
378
379    /// Execution context
380    pub execution_id: ExecutionId,
381    pub step_id: Option<StepId>,
382
383    /// Decision metadata
384    pub decision_type: DecisionType,
385    pub timestamp: chrono::DateTime<chrono::Utc>,
386
387    /// Decision outcome
388    pub outcome: String,
389    /// Confidence in decision (0-1)
390    pub confidence: f64,
391
392    /// Decision inputs
393    pub inputs: DecisionInput,
394
395    /// Alternatives considered
396    pub alternatives: Option<Vec<DecisionAlternative>>,
397
398    /// Model context (if LLM was used)
399    pub model_context: Option<ModelContext>,
400
401    /// Reasoning trace (optional explanation)
402    pub reasoning: Option<String>,
403}
404
405impl DecisionRecord {
406    /// Create a new DecisionRecord
407    pub fn new(
408        decision_type: DecisionType,
409        execution_id: ExecutionId,
410        outcome: impl Into<String>,
411        confidence: f64,
412        inputs: DecisionInput,
413    ) -> Self {
414        Self {
415            decision_id: format!("dec_{}", Ksuid::new(None, None)),
416            execution_id,
417            step_id: None,
418            decision_type,
419            timestamp: chrono::Utc::now(),
420            outcome: outcome.into(),
421            confidence,
422            inputs,
423            alternatives: None,
424            model_context: None,
425            reasoning: None,
426        }
427    }
428
429    /// Add step context
430    pub fn with_step(mut self, step_id: StepId) -> Self {
431        self.step_id = Some(step_id);
432        self
433    }
434
435    /// Add alternatives
436    pub fn with_alternatives(mut self, alternatives: Vec<DecisionAlternative>) -> Self {
437        self.alternatives = Some(alternatives);
438        self
439    }
440
441    /// Add model context
442    pub fn with_model_context(mut self, model_context: ModelContext) -> Self {
443        self.model_context = Some(model_context);
444        self
445    }
446
447    /// Add reasoning
448    pub fn with_reasoning(mut self, reasoning: impl Into<String>) -> Self {
449        self.reasoning = Some(reasoning.into());
450        self
451    }
452}
453
454// =============================================================================
455// Control Events (Governance)
456// =============================================================================
457
458/// ControlActor - Who/what initiated the control action
459#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
460#[serde(rename_all = "snake_case")]
461pub enum ControlActor {
462    /// System-initiated (timeout, policy)
463    System,
464    /// User-initiated (button click)
465    User,
466    /// Agent-initiated (self-regulation)
467    Agent,
468    /// Policy engine decision
469    PolicyEngine,
470}
471
472/// ControlAction - What control action was requested
473///
474/// @see packages/enact-schemas/src/streaming.schemas.ts - controlSignalTypeSchema
475#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
476#[serde(rename_all = "snake_case")]
477pub enum ControlAction {
478    /// Stop execution completely
479    Stop,
480    /// Pause execution
481    Pause,
482    /// Resume execution
483    Resume,
484    /// Cancel execution
485    Cancel,
486    /// Approve HITL request
487    Approve,
488    /// Deny HITL request
489    Deny,
490    /// Escalate to human
491    Escalate,
492}
493
494/// ControlOutcome - What happened as a result of the control action
495#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
496#[serde(rename_all = "snake_case")]
497pub enum ControlOutcome {
498    /// Action was allowed and applied
499    Allowed,
500    /// Action was denied by policy
501    Denied,
502    /// Action was modified/adapted
503    Modified,
504    /// Action was escalated for approval
505    Escalated,
506}
507
508/// ControlEvent - Governance event for control signals
509///
510/// Records who requested what action, why, and what the outcome was.
511/// Used for audit trails and compliance.
512#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct ControlEvent {
514    /// Unique event ID
515    pub event_id: String,
516
517    /// Execution context
518    pub execution_id: ExecutionId,
519    pub step_id: Option<StepId>,
520
521    /// Control metadata
522    pub timestamp: chrono::DateTime<chrono::Utc>,
523    pub actor: ControlActor,
524    pub action: ControlAction,
525
526    /// Reasoning
527    pub reason: String,
528
529    /// Outcome
530    pub outcome: ControlOutcome,
531
532    /// Actor details (User ID, agent ID, etc.)
533    pub actor_id: Option<String>,
534}
535
536impl ControlEvent {
537    /// Create a new ControlEvent
538    pub fn new(
539        actor: ControlActor,
540        action: ControlAction,
541        execution_id: ExecutionId,
542        reason: impl Into<String>,
543        outcome: ControlOutcome,
544    ) -> Self {
545        Self {
546            event_id: format!("ctrl_{}", Ksuid::new(None, None)),
547            execution_id,
548            step_id: None,
549            timestamp: chrono::Utc::now(),
550            actor,
551            action,
552            reason: reason.into(),
553            outcome,
554            actor_id: None,
555        }
556    }
557
558    /// Add step context
559    pub fn with_step(mut self, step_id: StepId) -> Self {
560        self.step_id = Some(step_id);
561        self
562    }
563
564    /// Add actor ID
565    pub fn with_actor_id(mut self, actor_id: impl Into<String>) -> Self {
566        self.actor_id = Some(actor_id.into());
567        self
568    }
569
570    // --- Factory methods for common control events ---
571
572    /// Create a pause control event
573    pub fn pause(
574        execution_id: ExecutionId,
575        actor: ControlActor,
576        reason: impl Into<String>,
577    ) -> Self {
578        Self::new(
579            actor,
580            ControlAction::Pause,
581            execution_id,
582            reason,
583            ControlOutcome::Allowed,
584        )
585    }
586
587    /// Create a resume control event
588    pub fn resume(
589        execution_id: ExecutionId,
590        actor: ControlActor,
591        reason: impl Into<String>,
592    ) -> Self {
593        Self::new(
594            actor,
595            ControlAction::Resume,
596            execution_id,
597            reason,
598            ControlOutcome::Allowed,
599        )
600    }
601
602    /// Create a cancel control event
603    pub fn cancel(
604        execution_id: ExecutionId,
605        actor: ControlActor,
606        reason: impl Into<String>,
607    ) -> Self {
608        Self::new(
609            actor,
610            ControlAction::Cancel,
611            execution_id,
612            reason,
613            ControlOutcome::Allowed,
614        )
615    }
616}
617
618// =============================================================================
619// Event (execution event with run/node context)
620// =============================================================================
621
622/// Event - execution event with run/node context
623#[derive(Debug, Clone, Serialize, Deserialize)]
624pub struct Event {
625    /// Event ID
626    pub id: String,
627    /// Run context (now ExecutionId)
628    pub run_id: ExecutionId,
629    /// Node context (now StepId)
630    pub node_id: Option<StepId>,
631    /// Event author (agent name, tool name, etc.)
632    pub author: String,
633    /// Timestamp
634    pub timestamp: chrono::DateTime<chrono::Utc>,
635    /// Event content
636    pub content: Option<String>,
637    /// Is this the final response?
638    pub is_final: bool,
639}
640
641impl Event {
642    pub fn new(run_id: ExecutionId, author: impl Into<String>) -> Self {
643        Self {
644            id: new_event_id(),
645            run_id,
646            node_id: None,
647            author: author.into(),
648            timestamp: chrono::Utc::now(),
649            content: None,
650            is_final: false,
651        }
652    }
653
654    pub fn with_content(mut self, content: impl Into<String>) -> Self {
655        self.content = Some(content.into());
656        self
657    }
658
659    pub fn with_node(mut self, node_id: StepId) -> Self {
660        self.node_id = Some(node_id);
661        self
662    }
663
664    pub fn final_response(mut self) -> Self {
665        self.is_final = true;
666        self
667    }
668}
669
670#[cfg(test)]
671mod tests {
672    use super::*;
673
674    // =========================================================================
675    // ExecutionEventType Tests
676    // =========================================================================
677
678    #[test]
679    fn test_execution_event_type_as_str_execution_start() {
680        assert_eq!(
681            ExecutionEventType::ExecutionStart.as_str(),
682            "execution.start"
683        );
684    }
685
686    #[test]
687    fn test_execution_event_type_as_str_execution_end() {
688        assert_eq!(ExecutionEventType::ExecutionEnd.as_str(), "execution.end");
689    }
690
691    #[test]
692    fn test_execution_event_type_as_str_execution_failed() {
693        assert_eq!(
694            ExecutionEventType::ExecutionFailed.as_str(),
695            "execution.failed"
696        );
697    }
698
699    #[test]
700    fn test_execution_event_type_as_str_execution_cancelled() {
701        assert_eq!(
702            ExecutionEventType::ExecutionCancelled.as_str(),
703            "execution.cancelled"
704        );
705    }
706
707    #[test]
708    fn test_execution_event_type_as_str_step_start() {
709        assert_eq!(ExecutionEventType::StepStart.as_str(), "step.start");
710    }
711
712    #[test]
713    fn test_execution_event_type_as_str_step_end() {
714        assert_eq!(ExecutionEventType::StepEnd.as_str(), "step.end");
715    }
716
717    #[test]
718    fn test_execution_event_type_as_str_step_failed() {
719        assert_eq!(ExecutionEventType::StepFailed.as_str(), "step.failed");
720    }
721
722    #[test]
723    fn test_execution_event_type_as_str_artifact_created() {
724        assert_eq!(
725            ExecutionEventType::ArtifactCreated.as_str(),
726            "artifact.created"
727        );
728    }
729
730    #[test]
731    fn test_execution_event_type_as_str_state_snapshot() {
732        assert_eq!(ExecutionEventType::StateSnapshot.as_str(), "state.snapshot");
733    }
734
735    #[test]
736    fn test_execution_event_type_as_str_decision_made() {
737        assert_eq!(ExecutionEventType::DecisionMade.as_str(), "decision.made");
738    }
739
740    #[test]
741    fn test_execution_event_type_as_str_control_pause() {
742        assert_eq!(ExecutionEventType::ControlPause.as_str(), "control.pause");
743    }
744
745    #[test]
746    fn test_execution_event_type_as_str_control_resume() {
747        assert_eq!(ExecutionEventType::ControlResume.as_str(), "control.resume");
748    }
749
750    #[test]
751    fn test_execution_event_type_as_str_control_cancel() {
752        assert_eq!(ExecutionEventType::ControlCancel.as_str(), "control.cancel");
753    }
754
755    #[test]
756    fn test_execution_event_type_serde() {
757        let event_type = ExecutionEventType::ExecutionStart;
758        let json = serde_json::to_string(&event_type).unwrap();
759        let parsed: ExecutionEventType = serde_json::from_str(&json).unwrap();
760        assert_eq!(event_type, parsed);
761    }
762
763    #[test]
764    fn test_execution_event_type_equality() {
765        assert_eq!(ExecutionEventType::StepStart, ExecutionEventType::StepStart);
766        assert_ne!(ExecutionEventType::StepStart, ExecutionEventType::StepEnd);
767    }
768
769    // =========================================================================
770    // ExecutionContext Tests
771    // =========================================================================
772
773    #[test]
774    fn test_execution_context_new() {
775        let exec_id = ExecutionId::from_string("exec_test");
776        let ctx = ExecutionContext::new(exec_id.clone());
777        assert_eq!(ctx.execution_id.as_str(), "exec_test");
778        assert!(ctx.step_id.is_none());
779        assert!(ctx.artifact_id.is_none());
780        assert!(ctx.parent.is_none());
781        assert!(ctx.tenant_id.is_none());
782        assert!(ctx.user_id.is_none());
783    }
784
785    #[test]
786    fn test_execution_context_with_step() {
787        let exec_id = ExecutionId::from_string("exec_test");
788        let step_id = StepId::from_string("step_test");
789        let ctx = ExecutionContext::new(exec_id).with_step(step_id.clone());
790        assert!(ctx.step_id.is_some());
791        assert_eq!(ctx.step_id.unwrap().as_str(), "step_test");
792    }
793
794    #[test]
795    fn test_execution_context_with_artifact() {
796        let exec_id = ExecutionId::from_string("exec_test");
797        let artifact_id = ArtifactId::from_string("artifact_test");
798        let ctx = ExecutionContext::new(exec_id).with_artifact(artifact_id);
799        assert!(ctx.artifact_id.is_some());
800        assert_eq!(ctx.artifact_id.unwrap().as_str(), "artifact_test");
801    }
802
803    #[test]
804    fn test_execution_context_with_parent() {
805        let exec_id = ExecutionId::from_string("exec_test");
806        let parent = ParentLink::from_user_message("msg_123");
807        let ctx = ExecutionContext::new(exec_id).with_parent(parent);
808        assert!(ctx.parent.is_some());
809        assert_eq!(ctx.parent.unwrap().parent_id, "msg_123");
810    }
811
812    #[test]
813    fn test_execution_context_with_tenant() {
814        let exec_id = ExecutionId::from_string("exec_test");
815        let tenant_id = TenantId::from_string("tenant_test");
816        let user_id = UserId::from_string("user_test");
817        let ctx = ExecutionContext::new(exec_id).with_tenant(tenant_id, Some(user_id));
818        assert!(ctx.tenant_id.is_some());
819        assert!(ctx.user_id.is_some());
820        assert_eq!(ctx.tenant_id.unwrap().as_str(), "tenant_test");
821        assert_eq!(ctx.user_id.unwrap().as_str(), "user_test");
822    }
823
824    #[test]
825    fn test_execution_context_builder_chain() {
826        let exec_id = ExecutionId::from_string("exec_chain");
827        let step_id = StepId::from_string("step_chain");
828        let artifact_id = ArtifactId::from_string("artifact_chain");
829        let parent = ParentLink::system();
830        let tenant_id = TenantId::from_string("tenant_chain");
831
832        let ctx = ExecutionContext::new(exec_id)
833            .with_step(step_id)
834            .with_artifact(artifact_id)
835            .with_parent(parent)
836            .with_tenant(tenant_id, None);
837
838        assert!(ctx.step_id.is_some());
839        assert!(ctx.artifact_id.is_some());
840        assert!(ctx.parent.is_some());
841        assert!(ctx.tenant_id.is_some());
842        assert!(ctx.user_id.is_none());
843    }
844
845    #[test]
846    fn test_execution_context_serde() {
847        let exec_id = ExecutionId::from_string("exec_serde");
848        let ctx = ExecutionContext::new(exec_id);
849        let json = serde_json::to_string(&ctx).unwrap();
850        let parsed: ExecutionContext = serde_json::from_str(&json).unwrap();
851        assert_eq!(ctx.execution_id.as_str(), parsed.execution_id.as_str());
852    }
853
854    // =========================================================================
855    // ExecutionEvent Tests
856    // =========================================================================
857
858    #[test]
859    fn test_execution_event_new() {
860        let ctx = ExecutionContext::new(ExecutionId::from_string("exec_test"));
861        let event = ExecutionEvent::new(ExecutionEventType::ExecutionStart, ctx);
862        assert!(event.event_id.starts_with("evt_"));
863        assert_eq!(event.event_type, ExecutionEventType::ExecutionStart);
864        assert!(event.duration_ms.is_none());
865        assert!(event.payload.is_none());
866    }
867
868    #[test]
869    fn test_execution_event_with_duration() {
870        let ctx = ExecutionContext::new(ExecutionId::from_string("exec_test"));
871        let event = ExecutionEvent::new(ExecutionEventType::StepEnd, ctx).with_duration(1500);
872        assert_eq!(event.duration_ms, Some(1500));
873    }
874
875    #[test]
876    fn test_execution_event_with_payload() {
877        let ctx = ExecutionContext::new(ExecutionId::from_string("exec_test"));
878        let payload = serde_json::json!({"key": "value"});
879        let event =
880            ExecutionEvent::new(ExecutionEventType::StepStart, ctx).with_payload(payload.clone());
881        assert!(event.payload.is_some());
882        assert_eq!(event.payload.unwrap(), payload);
883    }
884
885    #[test]
886    fn test_execution_event_execution_start() {
887        let exec_id = ExecutionId::from_string("exec_start");
888        let event = ExecutionEvent::execution_start(exec_id, None);
889        assert_eq!(event.event_type, ExecutionEventType::ExecutionStart);
890        assert!(event.context.parent.is_none());
891    }
892
893    #[test]
894    fn test_execution_event_execution_start_with_parent() {
895        let exec_id = ExecutionId::from_string("exec_start");
896        let parent = ParentLink::from_user_message("msg_trigger");
897        let event = ExecutionEvent::execution_start(exec_id, Some(parent));
898        assert_eq!(event.event_type, ExecutionEventType::ExecutionStart);
899        assert!(event.context.parent.is_some());
900    }
901
902    #[test]
903    fn test_execution_event_execution_end() {
904        let exec_id = ExecutionId::from_string("exec_end");
905        let event = ExecutionEvent::execution_end(exec_id, Some(5000));
906        assert_eq!(event.event_type, ExecutionEventType::ExecutionEnd);
907        assert_eq!(event.duration_ms, Some(5000));
908    }
909
910    #[test]
911    fn test_execution_event_step_start() {
912        let exec_id = ExecutionId::from_string("exec_step");
913        let step_id = StepId::from_string("step_start");
914        let event = ExecutionEvent::step_start(exec_id, step_id, StepType::LlmNode, "test_step");
915        assert_eq!(event.event_type, ExecutionEventType::StepStart);
916        assert!(event.payload.is_some());
917        let payload = event.payload.unwrap();
918        assert_eq!(payload["name"], "test_step");
919    }
920
921    #[test]
922    fn test_execution_event_step_end() {
923        let exec_id = ExecutionId::from_string("exec_step");
924        let step_id = StepId::from_string("step_end");
925        let event = ExecutionEvent::step_end(exec_id, step_id, 1000);
926        assert_eq!(event.event_type, ExecutionEventType::StepEnd);
927        assert_eq!(event.duration_ms, Some(1000));
928    }
929
930    #[test]
931    fn test_execution_event_artifact_created() {
932        let exec_id = ExecutionId::from_string("exec_artifact");
933        let step_id = StepId::from_string("step_artifact");
934        let artifact_id = ArtifactId::from_string("artifact_created");
935        let event = ExecutionEvent::artifact_created(exec_id, step_id, artifact_id, "code");
936        assert_eq!(event.event_type, ExecutionEventType::ArtifactCreated);
937        assert!(event.context.artifact_id.is_some());
938        assert!(event.payload.is_some());
939        assert_eq!(event.payload.unwrap()["artifact_type"], "code");
940    }
941
942    #[test]
943    fn test_execution_event_serde() {
944        let ctx = ExecutionContext::new(ExecutionId::from_string("exec_serde"));
945        let event = ExecutionEvent::new(ExecutionEventType::ExecutionStart, ctx);
946        let json = serde_json::to_string(&event).unwrap();
947        let parsed: ExecutionEvent = serde_json::from_str(&json).unwrap();
948        assert_eq!(event.event_type, parsed.event_type);
949    }
950
951    // =========================================================================
952    // DecisionType Tests
953    // =========================================================================
954
955    #[test]
956    fn test_decision_type_variants() {
957        let variants = vec![
958            DecisionType::Routing,
959            DecisionType::Branch,
960            DecisionType::Approval,
961            DecisionType::Escalation,
962            DecisionType::Retry,
963            DecisionType::Fallback,
964            DecisionType::PolicyEvaluation,
965            DecisionType::ToolSelection,
966            DecisionType::Rejection,
967        ];
968        for variant in variants {
969            let json = serde_json::to_string(&variant).unwrap();
970            let parsed: DecisionType = serde_json::from_str(&json).unwrap();
971            assert_eq!(variant, parsed);
972        }
973    }
974
975    #[test]
976    fn test_decision_type_equality() {
977        assert_eq!(DecisionType::Routing, DecisionType::Routing);
978        assert_ne!(DecisionType::Routing, DecisionType::Branch);
979    }
980
981    // =========================================================================
982    // DecisionInput Tests
983    // =========================================================================
984
985    #[test]
986    fn test_decision_input_creation() {
987        let input = DecisionInput {
988            facts: vec!["fact1".to_string(), "fact2".to_string()],
989            constraints: vec!["constraint1".to_string()],
990            evidence_ids: vec!["ev_123".to_string()],
991        };
992        assert_eq!(input.facts.len(), 2);
993        assert_eq!(input.constraints.len(), 1);
994        assert_eq!(input.evidence_ids.len(), 1);
995    }
996
997    #[test]
998    fn test_decision_input_serde() {
999        let input = DecisionInput {
1000            facts: vec!["fact1".to_string()],
1001            constraints: vec![],
1002            evidence_ids: vec![],
1003        };
1004        let json = serde_json::to_string(&input).unwrap();
1005        let parsed: DecisionInput = serde_json::from_str(&json).unwrap();
1006        assert_eq!(input.facts, parsed.facts);
1007    }
1008
1009    // =========================================================================
1010    // DecisionAlternative Tests
1011    // =========================================================================
1012
1013    #[test]
1014    fn test_decision_alternative_creation() {
1015        let alt = DecisionAlternative {
1016            option: "option_b".to_string(),
1017            score: 0.75,
1018            rejected_reason: "Lower confidence".to_string(),
1019        };
1020        assert_eq!(alt.option, "option_b");
1021        assert_eq!(alt.score, 0.75);
1022    }
1023
1024    #[test]
1025    fn test_decision_alternative_serde() {
1026        let alt = DecisionAlternative {
1027            option: "alt".to_string(),
1028            score: 0.5,
1029            rejected_reason: "reason".to_string(),
1030        };
1031        let json = serde_json::to_string(&alt).unwrap();
1032        let parsed: DecisionAlternative = serde_json::from_str(&json).unwrap();
1033        assert_eq!(alt.option, parsed.option);
1034        assert_eq!(alt.score, parsed.score);
1035    }
1036
1037    // =========================================================================
1038    // ModelContext Tests
1039    // =========================================================================
1040
1041    #[test]
1042    fn test_model_context_creation() {
1043        let ctx = ModelContext {
1044            provider: "anthropic".to_string(),
1045            model: "claude-3-opus".to_string(),
1046            version: Some("2024".to_string()),
1047            temperature: Some(0.7),
1048            max_tokens: Some(1000),
1049        };
1050        assert_eq!(ctx.provider, "anthropic");
1051        assert_eq!(ctx.model, "claude-3-opus");
1052        assert!(ctx.temperature.is_some());
1053    }
1054
1055    #[test]
1056    fn test_model_context_minimal() {
1057        let ctx = ModelContext {
1058            provider: "openai".to_string(),
1059            model: "gpt-4".to_string(),
1060            version: None,
1061            temperature: None,
1062            max_tokens: None,
1063        };
1064        assert!(ctx.version.is_none());
1065        assert!(ctx.temperature.is_none());
1066    }
1067
1068    #[test]
1069    fn test_model_context_serde() {
1070        let ctx = ModelContext {
1071            provider: "test".to_string(),
1072            model: "test-model".to_string(),
1073            version: None,
1074            temperature: Some(0.5),
1075            max_tokens: None,
1076        };
1077        let json = serde_json::to_string(&ctx).unwrap();
1078        let parsed: ModelContext = serde_json::from_str(&json).unwrap();
1079        assert_eq!(ctx.provider, parsed.provider);
1080        assert_eq!(ctx.temperature, parsed.temperature);
1081    }
1082
1083    // =========================================================================
1084    // DecisionRecord Tests
1085    // =========================================================================
1086
1087    #[test]
1088    fn test_decision_record_new() {
1089        let exec_id = ExecutionId::from_string("exec_decision");
1090        let input = DecisionInput {
1091            facts: vec!["fact".to_string()],
1092            constraints: vec![],
1093            evidence_ids: vec![],
1094        };
1095        let record = DecisionRecord::new(DecisionType::Routing, exec_id, "agent_a", 0.95, input);
1096        assert!(record.decision_id.starts_with("dec_"));
1097        assert_eq!(record.decision_type, DecisionType::Routing);
1098        assert_eq!(record.outcome, "agent_a");
1099        assert_eq!(record.confidence, 0.95);
1100    }
1101
1102    #[test]
1103    fn test_decision_record_with_step() {
1104        let exec_id = ExecutionId::from_string("exec_decision");
1105        let step_id = StepId::from_string("step_decision");
1106        let input = DecisionInput {
1107            facts: vec![],
1108            constraints: vec![],
1109            evidence_ids: vec![],
1110        };
1111        let record = DecisionRecord::new(DecisionType::Branch, exec_id, "yes", 0.8, input)
1112            .with_step(step_id);
1113        assert!(record.step_id.is_some());
1114    }
1115
1116    #[test]
1117    fn test_decision_record_with_alternatives() {
1118        let exec_id = ExecutionId::from_string("exec_decision");
1119        let input = DecisionInput {
1120            facts: vec![],
1121            constraints: vec![],
1122            evidence_ids: vec![],
1123        };
1124        let alternatives = vec![DecisionAlternative {
1125            option: "option_b".to_string(),
1126            score: 0.6,
1127            rejected_reason: "Lower score".to_string(),
1128        }];
1129        let record = DecisionRecord::new(DecisionType::Routing, exec_id, "option_a", 0.9, input)
1130            .with_alternatives(alternatives);
1131        assert!(record.alternatives.is_some());
1132        assert_eq!(record.alternatives.unwrap().len(), 1);
1133    }
1134
1135    #[test]
1136    fn test_decision_record_with_model_context() {
1137        let exec_id = ExecutionId::from_string("exec_decision");
1138        let input = DecisionInput {
1139            facts: vec![],
1140            constraints: vec![],
1141            evidence_ids: vec![],
1142        };
1143        let model_ctx = ModelContext {
1144            provider: "anthropic".to_string(),
1145            model: "claude".to_string(),
1146            version: None,
1147            temperature: None,
1148            max_tokens: None,
1149        };
1150        let record = DecisionRecord::new(DecisionType::Approval, exec_id, "approved", 1.0, input)
1151            .with_model_context(model_ctx);
1152        assert!(record.model_context.is_some());
1153    }
1154
1155    #[test]
1156    fn test_decision_record_with_reasoning() {
1157        let exec_id = ExecutionId::from_string("exec_decision");
1158        let input = DecisionInput {
1159            facts: vec![],
1160            constraints: vec![],
1161            evidence_ids: vec![],
1162        };
1163        let record =
1164            DecisionRecord::new(DecisionType::Escalation, exec_id, "escalated", 0.5, input)
1165                .with_reasoning("Confidence too low for autonomous action");
1166        assert!(record.reasoning.is_some());
1167        assert!(record.reasoning.unwrap().contains("Confidence"));
1168    }
1169
1170    #[test]
1171    fn test_decision_record_serde() {
1172        let exec_id = ExecutionId::from_string("exec_serde");
1173        let input = DecisionInput {
1174            facts: vec!["f".to_string()],
1175            constraints: vec![],
1176            evidence_ids: vec![],
1177        };
1178        let record = DecisionRecord::new(DecisionType::Retry, exec_id, "retry", 0.7, input);
1179        let json = serde_json::to_string(&record).unwrap();
1180        let parsed: DecisionRecord = serde_json::from_str(&json).unwrap();
1181        assert_eq!(record.decision_type, parsed.decision_type);
1182    }
1183
1184    // =========================================================================
1185    // ControlActor Tests
1186    // =========================================================================
1187
1188    #[test]
1189    fn test_control_actor_variants() {
1190        let variants = vec![
1191            ControlActor::System,
1192            ControlActor::User,
1193            ControlActor::Agent,
1194            ControlActor::PolicyEngine,
1195        ];
1196        for variant in variants {
1197            let json = serde_json::to_string(&variant).unwrap();
1198            let parsed: ControlActor = serde_json::from_str(&json).unwrap();
1199            assert_eq!(variant, parsed);
1200        }
1201    }
1202
1203    #[test]
1204    fn test_control_actor_equality() {
1205        assert_eq!(ControlActor::System, ControlActor::System);
1206        assert_ne!(ControlActor::System, ControlActor::User);
1207    }
1208
1209    // =========================================================================
1210    // ControlAction Tests
1211    // =========================================================================
1212
1213    #[test]
1214    fn test_control_action_variants() {
1215        let variants = vec![
1216            ControlAction::Stop,
1217            ControlAction::Pause,
1218            ControlAction::Resume,
1219            ControlAction::Cancel,
1220            ControlAction::Approve,
1221            ControlAction::Deny,
1222            ControlAction::Escalate,
1223        ];
1224        for variant in variants {
1225            let json = serde_json::to_string(&variant).unwrap();
1226            let parsed: ControlAction = serde_json::from_str(&json).unwrap();
1227            assert_eq!(variant, parsed);
1228        }
1229    }
1230
1231    // =========================================================================
1232    // ControlOutcome Tests
1233    // =========================================================================
1234
1235    #[test]
1236    fn test_control_outcome_variants() {
1237        let variants = vec![
1238            ControlOutcome::Allowed,
1239            ControlOutcome::Denied,
1240            ControlOutcome::Modified,
1241            ControlOutcome::Escalated,
1242        ];
1243        for variant in variants {
1244            let json = serde_json::to_string(&variant).unwrap();
1245            let parsed: ControlOutcome = serde_json::from_str(&json).unwrap();
1246            assert_eq!(variant, parsed);
1247        }
1248    }
1249
1250    // =========================================================================
1251    // ControlEvent Tests
1252    // =========================================================================
1253
1254    #[test]
1255    fn test_control_event_new() {
1256        let exec_id = ExecutionId::from_string("exec_ctrl");
1257        let event = ControlEvent::new(
1258            ControlActor::User,
1259            ControlAction::Pause,
1260            exec_id,
1261            "User requested pause",
1262            ControlOutcome::Allowed,
1263        );
1264        assert!(event.event_id.starts_with("ctrl_"));
1265        assert_eq!(event.actor, ControlActor::User);
1266        assert_eq!(event.action, ControlAction::Pause);
1267        assert_eq!(event.outcome, ControlOutcome::Allowed);
1268    }
1269
1270    #[test]
1271    fn test_control_event_with_step() {
1272        let exec_id = ExecutionId::from_string("exec_ctrl");
1273        let step_id = StepId::from_string("step_ctrl");
1274        let event = ControlEvent::new(
1275            ControlActor::System,
1276            ControlAction::Cancel,
1277            exec_id,
1278            "Timeout",
1279            ControlOutcome::Allowed,
1280        )
1281        .with_step(step_id);
1282        assert!(event.step_id.is_some());
1283    }
1284
1285    #[test]
1286    fn test_control_event_with_actor_id() {
1287        let exec_id = ExecutionId::from_string("exec_ctrl");
1288        let event = ControlEvent::new(
1289            ControlActor::User,
1290            ControlAction::Approve,
1291            exec_id,
1292            "Approved by admin",
1293            ControlOutcome::Allowed,
1294        )
1295        .with_actor_id("user_admin_123");
1296        assert!(event.actor_id.is_some());
1297        assert_eq!(event.actor_id.unwrap(), "user_admin_123");
1298    }
1299
1300    #[test]
1301    fn test_control_event_pause_factory() {
1302        let exec_id = ExecutionId::from_string("exec_pause");
1303        let event = ControlEvent::pause(exec_id, ControlActor::User, "Pausing for review");
1304        assert_eq!(event.action, ControlAction::Pause);
1305        assert_eq!(event.actor, ControlActor::User);
1306        assert_eq!(event.outcome, ControlOutcome::Allowed);
1307    }
1308
1309    #[test]
1310    fn test_control_event_resume_factory() {
1311        let exec_id = ExecutionId::from_string("exec_resume");
1312        let event = ControlEvent::resume(exec_id, ControlActor::User, "Resuming after review");
1313        assert_eq!(event.action, ControlAction::Resume);
1314    }
1315
1316    #[test]
1317    fn test_control_event_cancel_factory() {
1318        let exec_id = ExecutionId::from_string("exec_cancel");
1319        let event = ControlEvent::cancel(exec_id, ControlActor::System, "System timeout");
1320        assert_eq!(event.action, ControlAction::Cancel);
1321        assert_eq!(event.actor, ControlActor::System);
1322    }
1323
1324    #[test]
1325    fn test_control_event_serde() {
1326        let exec_id = ExecutionId::from_string("exec_serde");
1327        let event = ControlEvent::pause(exec_id, ControlActor::Agent, "Self-regulation");
1328        let json = serde_json::to_string(&event).unwrap();
1329        let parsed: ControlEvent = serde_json::from_str(&json).unwrap();
1330        assert_eq!(event.action, parsed.action);
1331        assert_eq!(event.actor, parsed.actor);
1332    }
1333
1334    // =========================================================================
1335    // Legacy Event Tests
1336    // =========================================================================
1337
1338    #[test]
1339    fn test_event_new() {
1340        let run_id = ExecutionId::from_string("exec_legacy");
1341        let event = Event::new(run_id, "test_author");
1342        assert!(event.id.starts_with("evt_"));
1343        assert_eq!(event.author, "test_author");
1344        assert!(!event.is_final);
1345        assert!(event.content.is_none());
1346        assert!(event.node_id.is_none());
1347    }
1348
1349    #[test]
1350    fn test_event_with_content() {
1351        let run_id = ExecutionId::from_string("exec_legacy");
1352        let event = Event::new(run_id, "author").with_content("Hello, world!");
1353        assert!(event.content.is_some());
1354        assert_eq!(event.content.unwrap(), "Hello, world!");
1355    }
1356
1357    #[test]
1358    fn test_event_with_node() {
1359        let run_id = ExecutionId::from_string("exec_legacy");
1360        let node_id = StepId::from_string("step_legacy");
1361        let event = Event::new(run_id, "author").with_node(node_id.clone());
1362        assert!(event.node_id.is_some());
1363        assert_eq!(event.node_id.unwrap().as_str(), "step_legacy");
1364    }
1365
1366    #[test]
1367    fn test_event_final_response() {
1368        let run_id = ExecutionId::from_string("exec_legacy");
1369        let event = Event::new(run_id, "author").final_response();
1370        assert!(event.is_final);
1371    }
1372
1373    #[test]
1374    fn test_event_builder_chain() {
1375        let run_id = ExecutionId::from_string("exec_chain");
1376        let node_id = StepId::from_string("step_chain");
1377        let event = Event::new(run_id, "test")
1378            .with_content("Content here")
1379            .with_node(node_id)
1380            .final_response();
1381        assert!(event.content.is_some());
1382        assert!(event.node_id.is_some());
1383        assert!(event.is_final);
1384    }
1385
1386    #[test]
1387    fn test_event_serde() {
1388        let run_id = ExecutionId::from_string("exec_serde");
1389        let event = Event::new(run_id, "author").with_content("test");
1390        let json = serde_json::to_string(&event).unwrap();
1391        let parsed: Event = serde_json::from_str(&json).unwrap();
1392        assert_eq!(event.author, parsed.author);
1393    }
1394
1395    // =========================================================================
1396    // Event ID Format Tests
1397    // =========================================================================
1398
1399    #[test]
1400    fn test_event_id_format() {
1401        let ctx = ExecutionContext::new(ExecutionId::from_string("exec_test"));
1402        let event = ExecutionEvent::new(ExecutionEventType::ExecutionStart, ctx);
1403        assert!(event.event_id.starts_with("evt_"));
1404        // evt_ (4 chars) + KSUID (27 chars) = 31 chars
1405        assert_eq!(event.event_id.len(), 31);
1406    }
1407
1408    #[test]
1409    fn test_decision_id_format() {
1410        let exec_id = ExecutionId::from_string("exec_test");
1411        let input = DecisionInput {
1412            facts: vec![],
1413            constraints: vec![],
1414            evidence_ids: vec![],
1415        };
1416        let record = DecisionRecord::new(DecisionType::Routing, exec_id, "outcome", 0.9, input);
1417        assert!(record.decision_id.starts_with("dec_"));
1418    }
1419
1420    #[test]
1421    fn test_control_event_id_format() {
1422        let exec_id = ExecutionId::from_string("exec_test");
1423        let event = ControlEvent::pause(exec_id, ControlActor::User, "test");
1424        assert!(event.event_id.starts_with("ctrl_"));
1425    }
1426}