Skip to main content

enact_core/kernel/
execution_model.rs

1//! Execution Model - Runtime instance types for graph/agent execution
2//!
3//! This module defines the core model types:
4//! - `Execution`: One run of a blueprint (graph, agent, workflow)
5//! - `Step`: A distinct action within an execution
6//!
7//! These are the **model types** that hold execution state.
8//! The **engine** (ExecutionKernel) uses these models to manage execution.
9//!
10//! ## ⚠️ CRITICAL INVARIANT: Data + Bookkeeping Only
11//!
12//! **This module MUST NOT contain execution logic or side effects.**
13//!
14//! This module is strictly for:
15//! - Data structures (Execution, Step)
16//! - Bookkeeping methods (getters, setters, state queries)
17//!
18//! This module MUST NEVER:
19//! - Execute logic (no business logic, no decision-making)
20//! - Call providers (no external service calls)
21//! - Embed policy checks (policy belongs in kernel::enforcement)
22//! - Perform side effects (no I/O, no mutations beyond self)
23//!
24//! If you find yourself adding execution logic here, it belongs in:
25//! - `kernel::reducer` (for state transitions)
26//! - `kernel::enforcement` (for policy checks)
27//! - `kernel::execution_kernel` (for orchestration)
28//!
29//! ## Error Handling (feat-02)
30//!
31//! All errors are represented using `ExecutionError` which provides:
32//! - Deterministic retry policies
33//! - Structured error categories
34//! - HTTP status code mapping
35//! - Idempotency tracking
36
37use super::error::ExecutionError;
38use super::execution_state::{ExecutionState, StepState};
39use super::ids::{CallableType, ExecutionId, ParentLink, StepId, StepSource, StepType, TenantId};
40use serde::{Deserialize, Serialize};
41use std::collections::HashMap;
42use std::time::Instant;
43
44/// Execution - one run of a blueprint
45#[derive(Debug)]
46pub struct Execution {
47    /// Unique execution ID
48    pub id: ExecutionId,
49    /// Tenant ID (REQUIRED for audit trail and multi-tenant isolation)
50    /// This is set from TenantContext when the kernel is created.
51    pub tenant_id: Option<TenantId>,
52    /// Current state in the lifecycle
53    pub state: ExecutionState,
54    /// Parent execution (if this is a sub-execution)
55    pub parent: Option<ParentLink>,
56    /// All steps in this execution
57    pub steps: HashMap<StepId, Step>,
58    /// Ordered list of step IDs (for replay)
59    pub step_order: Vec<StepId>,
60    /// Schema version hash (for replay validation)
61    pub schema_version: Option<String>,
62    /// Start time
63    pub started_at: Option<Instant>,
64    /// End time
65    pub ended_at: Option<Instant>,
66    /// Final output (if completed)
67    pub output: Option<String>,
68    /// Structured error (if failed) - see feat-02 Error Taxonomy
69    pub error: Option<ExecutionError>,
70}
71
72impl Execution {
73    /// Create a new execution
74    pub fn new() -> Self {
75        Self {
76            id: ExecutionId::new(),
77            tenant_id: None,
78            state: ExecutionState::Created,
79            parent: None,
80            steps: HashMap::new(),
81            step_order: Vec::new(),
82            schema_version: None,
83            started_at: None,
84            ended_at: None,
85            output: None,
86            error: None,
87        }
88    }
89
90    /// Create a new execution with a specific ID
91    pub fn with_id(id: ExecutionId) -> Self {
92        Self {
93            id,
94            tenant_id: None,
95            state: ExecutionState::Created,
96            parent: None,
97            steps: HashMap::new(),
98            step_order: Vec::new(),
99            schema_version: None,
100            started_at: None,
101            ended_at: None,
102            output: None,
103            error: None,
104        }
105    }
106
107    /// Create a new execution with tenant ID
108    pub fn with_tenant(tenant_id: TenantId) -> Self {
109        Self {
110            id: ExecutionId::new(),
111            tenant_id: Some(tenant_id),
112            state: ExecutionState::Created,
113            parent: None,
114            steps: HashMap::new(),
115            step_order: Vec::new(),
116            schema_version: None,
117            started_at: None,
118            ended_at: None,
119            output: None,
120            error: None,
121        }
122    }
123
124    /// Create a child execution
125    /// Inherits tenant_id from parent for multi-tenant isolation
126    pub fn child(&self) -> Self {
127        let mut child = Self::new();
128        child.parent = Some(ParentLink::execution(self.id.clone()));
129        child.tenant_id = self.tenant_id.clone(); // Inherit tenant from parent
130        child
131    }
132
133    /// Set schema version for replay validation
134    pub fn with_schema_version(mut self, version: impl Into<String>) -> Self {
135        self.schema_version = Some(version.into());
136        self
137    }
138
139    /// Get a step by ID
140    pub fn get_step(&self, id: &StepId) -> Option<&Step> {
141        self.steps.get(id)
142    }
143
144    /// Get a mutable step by ID
145    pub fn get_step_mut(&mut self, id: &StepId) -> Option<&mut Step> {
146        self.steps.get_mut(id)
147    }
148
149    /// Add a new step
150    pub fn add_step(&mut self, step: Step) {
151        self.step_order.push(step.id.clone());
152        self.steps.insert(step.id.clone(), step);
153    }
154
155    /// Get execution duration in milliseconds
156    pub fn duration_ms(&self) -> Option<u64> {
157        match (self.started_at, self.ended_at) {
158            (Some(start), Some(end)) => Some(end.duration_since(start).as_millis() as u64),
159            (Some(start), None) => Some(start.elapsed().as_millis() as u64),
160            _ => None,
161        }
162    }
163
164    /// Check if execution is in a terminal state
165    pub fn is_terminal(&self) -> bool {
166        self.state.is_terminal()
167    }
168}
169
170impl Default for Execution {
171    fn default() -> Self {
172        Self::new()
173    }
174}
175
176/// Step - a distinct action within an execution
177///
178/// @see packages/enact-schemas/src/streaming.schemas.ts - stepEventDataSchema
179#[derive(Debug, Clone, Serialize, Deserialize)]
180#[serde(rename_all = "camelCase")]
181pub struct Step {
182    /// Unique step ID
183    #[serde(rename = "stepId")]
184    pub id: StepId,
185    /// Parent step (for nested operations)
186    #[serde(rename = "parentStepId")]
187    pub parent_step_id: Option<StepId>,
188    /// Step type
189    pub step_type: StepType,
190    /// Step name/label
191    pub name: String,
192    /// Current state (matches `state` in TypeScript schema)
193    pub state: StepState,
194    /// Input to this step
195    pub input: Option<String>,
196    /// Output from this step
197    pub output: Option<String>,
198    /// Structured error (if failed) - see feat-02 Error Taxonomy
199    pub error: Option<ExecutionError>,
200    /// Duration in milliseconds
201    pub duration_ms: Option<u64>,
202    /// Timestamp when step started (unix millis)
203    pub started_at: Option<i64>,
204    /// Timestamp when step ended (unix millis)
205    pub ended_at: Option<i64>,
206    /// Source/origin of this step (how/why it was created)
207    #[serde(skip_serializing_if = "Option::is_none")]
208    pub source: Option<StepSource>,
209    /// Callable ID (stable identifier for billing/traceability)
210    /// Unlike `name` which can change, this provides a stable reference.
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub callable_id: Option<String>,
213    /// Callable type (for billing differentiation and audit trails)
214    #[serde(skip_serializing_if = "Option::is_none")]
215    pub callable_type: Option<CallableType>,
216}
217
218impl Step {
219    /// Create a new step
220    pub fn new(step_type: StepType, name: impl Into<String>) -> Self {
221        Self {
222            id: StepId::new(),
223            parent_step_id: None,
224            step_type,
225            name: name.into(),
226            state: StepState::Pending,
227            input: None,
228            output: None,
229            error: None,
230            duration_ms: None,
231            started_at: None,
232            ended_at: None,
233            source: None,
234            callable_id: None,
235            callable_type: None,
236        }
237    }
238
239    /// Create a nested step under a parent
240    pub fn nested(parent_id: &StepId, step_type: StepType, name: impl Into<String>) -> Self {
241        let mut step = Self::new(step_type, name);
242        step.parent_step_id = Some(parent_id.clone());
243        step
244    }
245
246    /// Set input
247    pub fn with_input(mut self, input: impl Into<String>) -> Self {
248        self.input = Some(input.into());
249        self
250    }
251
252    /// Set source/origin of this step
253    pub fn with_source(mut self, source: StepSource) -> Self {
254        self.source = Some(source);
255        self
256    }
257
258    /// Set callable info (for billing/traceability)
259    ///
260    /// Unlike `name` which can change, callable_id provides a stable reference
261    /// for cost attribution and audit trails.
262    pub fn with_callable(
263        mut self,
264        callable_id: impl Into<String>,
265        callable_type: CallableType,
266    ) -> Self {
267        self.callable_id = Some(callable_id.into());
268        self.callable_type = Some(callable_type);
269        self
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::super::execution_state::WaitReason;
276    use super::super::ids::StepSourceType;
277    use super::*;
278
279    // =========================================================================
280    // Execution Tests
281    // =========================================================================
282
283    #[test]
284    fn test_execution_new() {
285        let exec = Execution::new();
286        assert!(exec.id.as_str().starts_with("exec_"));
287        assert_eq!(exec.state, ExecutionState::Created);
288        assert!(exec.parent.is_none());
289        assert!(exec.steps.is_empty());
290        assert!(exec.step_order.is_empty());
291        assert!(exec.schema_version.is_none());
292        assert!(exec.started_at.is_none());
293        assert!(exec.ended_at.is_none());
294        assert!(exec.output.is_none());
295        assert!(exec.error.is_none());
296    }
297
298    #[test]
299    fn test_execution_with_id() {
300        let id = ExecutionId::from_string("exec_custom_id");
301        let exec = Execution::with_id(id.clone());
302        assert_eq!(exec.id.as_str(), "exec_custom_id");
303        assert_eq!(exec.state, ExecutionState::Created);
304    }
305
306    #[test]
307    fn test_execution_child() {
308        let parent = Execution::new();
309        let child = parent.child();
310
311        assert!(child.parent.is_some());
312        let parent_link = child.parent.unwrap();
313        assert_eq!(parent_link.parent_id, parent.id.as_str());
314    }
315
316    #[test]
317    fn test_execution_with_schema_version() {
318        let exec = Execution::new().with_schema_version("v1.0.0");
319        assert_eq!(exec.schema_version, Some("v1.0.0".to_string()));
320    }
321
322    #[test]
323    fn test_execution_with_schema_version_owned_string() {
324        let exec = Execution::new().with_schema_version(String::from("v2.0.0"));
325        assert_eq!(exec.schema_version, Some("v2.0.0".to_string()));
326    }
327
328    #[test]
329    fn test_execution_add_step() {
330        let mut exec = Execution::new();
331        let step = Step::new(StepType::LlmNode, "test_step");
332        let step_id = step.id.clone();
333
334        exec.add_step(step);
335
336        assert_eq!(exec.steps.len(), 1);
337        assert_eq!(exec.step_order.len(), 1);
338        assert!(exec.steps.contains_key(&step_id));
339    }
340
341    #[test]
342    fn test_execution_get_step() {
343        let mut exec = Execution::new();
344        let step = Step::new(StepType::ToolNode, "get_step_test");
345        let step_id = step.id.clone();
346        exec.add_step(step);
347
348        let retrieved = exec.get_step(&step_id);
349        assert!(retrieved.is_some());
350        assert_eq!(retrieved.unwrap().name, "get_step_test");
351    }
352
353    #[test]
354    fn test_execution_get_step_not_found() {
355        let exec = Execution::new();
356        let nonexistent_id = StepId::from_string("step_nonexistent");
357        assert!(exec.get_step(&nonexistent_id).is_none());
358    }
359
360    #[test]
361    fn test_execution_get_step_mut() {
362        let mut exec = Execution::new();
363        let step = Step::new(StepType::FunctionNode, "mutable_step");
364        let step_id = step.id.clone();
365        exec.add_step(step);
366
367        let step_mut = exec.get_step_mut(&step_id);
368        assert!(step_mut.is_some());
369
370        step_mut.unwrap().output = Some("modified".to_string());
371
372        let step = exec.get_step(&step_id).unwrap();
373        assert_eq!(step.output, Some("modified".to_string()));
374    }
375
376    #[test]
377    fn test_execution_step_order_preserved() {
378        let mut exec = Execution::new();
379        let step1 = Step::new(StepType::LlmNode, "step1");
380        let step2 = Step::new(StepType::ToolNode, "step2");
381        let step3 = Step::new(StepType::FunctionNode, "step3");
382
383        let id1 = step1.id.clone();
384        let id2 = step2.id.clone();
385        let id3 = step3.id.clone();
386
387        exec.add_step(step1);
388        exec.add_step(step2);
389        exec.add_step(step3);
390
391        assert_eq!(exec.step_order[0], id1);
392        assert_eq!(exec.step_order[1], id2);
393        assert_eq!(exec.step_order[2], id3);
394    }
395
396    #[test]
397    fn test_execution_duration_ms_not_started() {
398        let exec = Execution::new();
399        assert!(exec.duration_ms().is_none());
400    }
401
402    #[test]
403    fn test_execution_duration_ms_started_not_ended() {
404        let mut exec = Execution::new();
405        exec.started_at = Some(Instant::now());
406        std::thread::sleep(std::time::Duration::from_millis(10));
407
408        let duration = exec.duration_ms();
409        assert!(duration.is_some());
410        assert!(duration.unwrap() >= 10);
411    }
412
413    #[test]
414    fn test_execution_duration_ms_completed() {
415        let mut exec = Execution::new();
416        let start = Instant::now();
417        std::thread::sleep(std::time::Duration::from_millis(20));
418        let end = Instant::now();
419
420        exec.started_at = Some(start);
421        exec.ended_at = Some(end);
422
423        let duration = exec.duration_ms();
424        assert!(duration.is_some());
425        assert!(duration.unwrap() >= 20);
426    }
427
428    #[test]
429    fn test_execution_is_terminal_created() {
430        let exec = Execution::new();
431        assert!(!exec.is_terminal());
432    }
433
434    #[test]
435    fn test_execution_is_terminal_running() {
436        let mut exec = Execution::new();
437        exec.state = ExecutionState::Running;
438        assert!(!exec.is_terminal());
439    }
440
441    #[test]
442    fn test_execution_is_terminal_completed() {
443        let mut exec = Execution::new();
444        exec.state = ExecutionState::Completed;
445        assert!(exec.is_terminal());
446    }
447
448    #[test]
449    fn test_execution_is_terminal_failed() {
450        let mut exec = Execution::new();
451        exec.state = ExecutionState::Failed;
452        assert!(exec.is_terminal());
453    }
454
455    #[test]
456    fn test_execution_is_terminal_cancelled() {
457        let mut exec = Execution::new();
458        exec.state = ExecutionState::Cancelled;
459        assert!(exec.is_terminal());
460    }
461
462    #[test]
463    fn test_execution_is_terminal_paused() {
464        let mut exec = Execution::new();
465        exec.state = ExecutionState::Paused;
466        assert!(!exec.is_terminal());
467    }
468
469    #[test]
470    fn test_execution_is_terminal_waiting() {
471        let mut exec = Execution::new();
472        exec.state = ExecutionState::Waiting(WaitReason::Approval);
473        assert!(!exec.is_terminal());
474    }
475
476    #[test]
477    fn test_execution_default() {
478        let exec: Execution = Default::default();
479        assert!(exec.id.as_str().starts_with("exec_"));
480        assert_eq!(exec.state, ExecutionState::Created);
481    }
482
483    // =========================================================================
484    // Step Tests
485    // =========================================================================
486
487    #[test]
488    fn test_step_new() {
489        let step = Step::new(StepType::LlmNode, "test_step");
490        assert!(step.id.as_str().starts_with("step_"));
491        assert_eq!(step.step_type, StepType::LlmNode);
492        assert_eq!(step.name, "test_step");
493        assert_eq!(step.state, StepState::Pending);
494        assert!(step.parent_step_id.is_none());
495        assert!(step.input.is_none());
496        assert!(step.output.is_none());
497        assert!(step.error.is_none());
498        assert!(step.duration_ms.is_none());
499    }
500
501    #[test]
502    fn test_step_new_all_types() {
503        let types = vec![
504            StepType::LlmNode,
505            StepType::GraphNode,
506            StepType::ToolNode,
507            StepType::FunctionNode,
508            StepType::RouterNode,
509            StepType::BranchNode,
510            StepType::LoopNode,
511        ];
512
513        for step_type in types {
514            let step = Step::new(step_type.clone(), "test");
515            assert_eq!(step.step_type, step_type);
516        }
517    }
518
519    #[test]
520    fn test_step_nested() {
521        let parent_id = StepId::from_string("step_parent");
522        let step = Step::nested(&parent_id, StepType::ToolNode, "nested_step");
523
524        assert!(step.parent_step_id.is_some());
525        assert_eq!(step.parent_step_id.unwrap().as_str(), "step_parent");
526        assert_eq!(step.step_type, StepType::ToolNode);
527        assert_eq!(step.name, "nested_step");
528    }
529
530    #[test]
531    fn test_step_with_input() {
532        let step = Step::new(StepType::LlmNode, "input_step").with_input("Hello, AI!");
533
534        assert!(step.input.is_some());
535        assert_eq!(step.input.unwrap(), "Hello, AI!");
536    }
537
538    #[test]
539    fn test_step_with_input_owned_string() {
540        let step =
541            Step::new(StepType::LlmNode, "input_step").with_input(String::from("Owned input"));
542
543        assert!(step.input.is_some());
544        assert_eq!(step.input.unwrap(), "Owned input");
545    }
546
547    #[test]
548    fn test_step_clone() {
549        let step = Step::new(StepType::FunctionNode, "cloneable").with_input("input data");
550        let cloned = step.clone();
551
552        assert_eq!(step.id.as_str(), cloned.id.as_str());
553        assert_eq!(step.name, cloned.name);
554        assert_eq!(step.input, cloned.input);
555    }
556
557    #[test]
558    fn test_step_serde() {
559        let step = Step::new(StepType::GraphNode, "serializable").with_input("input");
560
561        let json = serde_json::to_string(&step).unwrap();
562        let parsed: Step = serde_json::from_str(&json).unwrap();
563
564        assert_eq!(step.name, parsed.name);
565        assert_eq!(step.step_type, parsed.step_type);
566        assert_eq!(step.input, parsed.input);
567    }
568
569    #[test]
570    fn test_step_serde_field_names() {
571        // Verify JSON field names match TypeScript schema (camelCase)
572        let step = Step::new(StepType::LlmNode, "test_step").with_input("test input");
573
574        let json = serde_json::to_string(&step).unwrap();
575
576        // Check that camelCase field names are used
577        assert!(json.contains("\"stepId\""), "Should have stepId field");
578        assert!(json.contains("\"stepType\""), "Should have stepType field");
579        assert!(json.contains("\"state\""), "Should have state field");
580        assert!(
581            json.contains("\"durationMs\""),
582            "Should have durationMs field"
583        );
584        assert!(
585            json.contains("\"startedAt\""),
586            "Should have startedAt field"
587        );
588        assert!(json.contains("\"endedAt\""), "Should have endedAt field");
589
590        // Verify no snake_case field names
591        assert!(
592            !json.contains("\"step_id\""),
593            "Should NOT have step_id field"
594        );
595        assert!(
596            !json.contains("\"step_type\""),
597            "Should NOT have step_type field"
598        );
599        assert!(
600            !json.contains("\"duration_ms\""),
601            "Should NOT have duration_ms field"
602        );
603        assert!(
604            !json.contains("\"started_at\""),
605            "Should NOT have started_at field"
606        );
607        assert!(
608            !json.contains("\"ended_at\""),
609            "Should NOT have ended_at field"
610        );
611    }
612
613    #[test]
614    fn test_step_timestamps() {
615        let mut step = Step::new(StepType::ToolNode, "timestamped");
616        let now = chrono::Utc::now().timestamp_millis();
617
618        step.started_at = Some(now);
619        step.ended_at = Some(now + 1000);
620        step.duration_ms = Some(1000);
621
622        assert_eq!(step.started_at, Some(now));
623        assert_eq!(step.ended_at, Some(now + 1000));
624        assert_eq!(step.duration_ms, Some(1000));
625    }
626
627    #[test]
628    fn test_step_state_modifications() {
629        let mut step = Step::new(StepType::LlmNode, "state_step");
630
631        assert_eq!(step.state, StepState::Pending);
632
633        step.state = StepState::Running;
634        assert_eq!(step.state, StepState::Running);
635
636        step.state = StepState::Completed;
637        step.output = Some("Result".to_string());
638        assert_eq!(step.state, StepState::Completed);
639        assert_eq!(step.output, Some("Result".to_string()));
640    }
641
642    #[test]
643    fn test_step_error_handling() {
644        let mut step = Step::new(StepType::ToolNode, "error_step");
645
646        step.state = StepState::Failed;
647        step.error = Some(ExecutionError::kernel_internal("Test error"));
648
649        assert!(step.error.is_some());
650    }
651
652    // =========================================================================
653    // Integration Tests
654    // =========================================================================
655
656    #[test]
657    fn test_execution_with_multiple_steps() {
658        let mut exec = Execution::new();
659        exec.state = ExecutionState::Running;
660        exec.started_at = Some(Instant::now());
661
662        // Add multiple steps
663        for i in 0..5 {
664            let step = Step::new(StepType::FunctionNode, format!("step_{}", i))
665                .with_input(format!("input_{}", i));
666            exec.add_step(step);
667        }
668
669        assert_eq!(exec.steps.len(), 5);
670        assert_eq!(exec.step_order.len(), 5);
671
672        // Verify all steps are accessible
673        for step_id in &exec.step_order {
674            assert!(exec.get_step(step_id).is_some());
675        }
676    }
677
678    #[test]
679    fn test_nested_execution_structure() {
680        let root = Execution::new();
681        let child1 = root.child();
682        let child2 = root.child();
683
684        // Both children should have the same parent
685        assert!(child1.parent.is_some());
686        assert!(child2.parent.is_some());
687        assert_eq!(
688            child1.parent.as_ref().unwrap().parent_id,
689            child2.parent.as_ref().unwrap().parent_id
690        );
691
692        // But different IDs
693        assert_ne!(child1.id.as_str(), child2.id.as_str());
694    }
695
696    #[test]
697    fn test_nested_steps_structure() {
698        let parent_step = Step::new(StepType::GraphNode, "parent");
699        let parent_id = parent_step.id.clone();
700
701        let child1 = Step::nested(&parent_id, StepType::LlmNode, "child1");
702        let child2 = Step::nested(&parent_id, StepType::ToolNode, "child2");
703
704        assert_eq!(
705            child1.parent_step_id.as_ref().unwrap().as_str(),
706            parent_id.as_str()
707        );
708        assert_eq!(
709            child2.parent_step_id.as_ref().unwrap().as_str(),
710            parent_id.as_str()
711        );
712    }
713
714    // =========================================================================
715    // Callable Tracking Tests (for billing/traceability)
716    // =========================================================================
717
718    #[test]
719    fn test_step_with_callable() {
720        let step = Step::new(StepType::GraphNode, "Research Agent")
721            .with_callable("research-agent-v2", CallableType::Agent);
722
723        assert!(step.callable_id.is_some());
724        assert_eq!(step.callable_id.unwrap(), "research-agent-v2");
725        assert!(step.callable_type.is_some());
726        assert_eq!(step.callable_type.unwrap(), CallableType::Agent);
727    }
728
729    #[test]
730    fn test_step_callable_serde() {
731        let step = Step::new(StepType::GraphNode, "Chat Handler")
732            .with_callable("chat-handler", CallableType::Chat);
733
734        let json = serde_json::to_string(&step).unwrap();
735        let parsed: Step = serde_json::from_str(&json).unwrap();
736
737        assert_eq!(parsed.callable_id, Some("chat-handler".to_string()));
738        assert_eq!(parsed.callable_type, Some(CallableType::Chat));
739
740        // Verify camelCase field names
741        assert!(
742            json.contains("\"callableId\""),
743            "Should have callableId field"
744        );
745        assert!(
746            json.contains("\"callableType\""),
747            "Should have callableType field"
748        );
749    }
750
751    #[test]
752    fn test_step_callable_none_not_serialized() {
753        let step = Step::new(StepType::LlmNode, "No Callable Info");
754
755        let json = serde_json::to_string(&step).unwrap();
756
757        // Optional None fields should not appear in JSON (skip_serializing_if)
758        assert!(
759            !json.contains("callableId"),
760            "Should NOT serialize None callableId"
761        );
762        assert!(
763            !json.contains("callableType"),
764            "Should NOT serialize None callableType"
765        );
766    }
767
768    #[test]
769    fn test_callable_type_display() {
770        assert_eq!(format!("{}", CallableType::Completion), "completion");
771        assert_eq!(format!("{}", CallableType::Chat), "chat");
772        assert_eq!(format!("{}", CallableType::Agent), "agent");
773        assert_eq!(format!("{}", CallableType::Workflow), "workflow");
774        assert_eq!(format!("{}", CallableType::Background), "background");
775        assert_eq!(format!("{}", CallableType::Composite), "composite");
776        assert_eq!(format!("{}", CallableType::Tool), "tool");
777        assert_eq!(format!("{}", CallableType::Custom), "custom");
778    }
779
780    #[test]
781    fn test_callable_type_default() {
782        let default_type = CallableType::default();
783        assert_eq!(default_type, CallableType::Agent);
784    }
785
786    #[test]
787    fn test_callable_type_serde_all_variants() {
788        let variants = vec![
789            CallableType::Completion,
790            CallableType::Chat,
791            CallableType::Agent,
792            CallableType::Workflow,
793            CallableType::Background,
794            CallableType::Composite,
795            CallableType::Tool,
796            CallableType::Custom,
797        ];
798
799        for variant in variants {
800            let json = serde_json::to_string(&variant).unwrap();
801            let parsed: CallableType = serde_json::from_str(&json).unwrap();
802            assert_eq!(parsed, variant);
803        }
804    }
805
806    #[test]
807    fn test_step_chained_builders() {
808        // Test that all builders can be chained
809        let _parent_id = StepId::from_string("step_parent");
810        let step = Step::new(StepType::GraphNode, "Full Step")
811            .with_input("User request")
812            .with_callable("research-agent", CallableType::Agent)
813            .with_source(StepSource {
814                source_type: StepSourceType::Discovered,
815                triggered_by: Some("step_123".to_string()),
816                reason: Some("LLM suggested sub-task".to_string()),
817                depth: Some(1),
818                spawn_mode: None,
819            });
820
821        assert_eq!(step.name, "Full Step");
822        assert_eq!(step.input, Some("User request".to_string()));
823        assert_eq!(step.callable_id, Some("research-agent".to_string()));
824        assert_eq!(step.callable_type, Some(CallableType::Agent));
825        assert!(step.source.is_some());
826        assert_eq!(
827            step.source.as_ref().unwrap().source_type,
828            StepSourceType::Discovered
829        );
830    }
831}