Skip to main content

nika_engine/runtime/
context.rs

1//! WorkflowMeta — read-only workflow metadata for execution.
2//!
3//! Inspired by rustc's Session pattern. Created once from AnalyzedWorkflow,
4//! shared via Arc across all runtime components.
5//!
6//! This provides bidirectional TaskId ↔ name mapping and workflow-level
7//! defaults (provider, model) without requiring mutable access.
8
9use std::sync::Arc;
10
11use crate::ast::analyzed::{AnalyzedWorkflow, TaskId, TaskTable};
12
13/// Read-only workflow metadata — immutable after construction.
14///
15/// Created once from `AnalyzedWorkflow` and wrapped in `Arc` for sharing
16/// across runner, executor, and spawned tasks.
17#[derive(Debug)]
18pub struct WorkflowMeta {
19    /// Bidirectional TaskId ↔ name mapping.
20    task_table: TaskTable,
21
22    /// Default provider for the workflow.
23    provider: Option<String>,
24
25    /// Default model for the workflow.
26    model: Option<String>,
27}
28
29impl WorkflowMeta {
30    /// Create from an AnalyzedWorkflow, wrapped in Arc for sharing.
31    pub fn from_workflow(wf: &AnalyzedWorkflow) -> Arc<Self> {
32        Arc::new(Self {
33            task_table: wf.task_table.clone(),
34            provider: wf.provider.clone(),
35            model: wf.model.clone(),
36        })
37    }
38
39    /// Resolve TaskId to human-readable name.
40    ///
41    /// Returns `None` if the ID is not in the table (should not happen when
42    /// IDs originate from the analyzer, but callers can handle gracefully).
43    pub fn task_name(&self, id: TaskId) -> Option<&str> {
44        self.task_table.get_name(id)
45    }
46
47    /// Resolve name to TaskId. Returns None for unknown names.
48    pub fn task_id(&self, name: &str) -> Option<TaskId> {
49        self.task_table.get_id(name)
50    }
51
52    /// Default provider for the workflow.
53    pub fn provider(&self) -> Option<&str> {
54        self.provider.as_deref()
55    }
56
57    /// Default model for the workflow.
58    pub fn model(&self) -> Option<&str> {
59        self.model.as_deref()
60    }
61
62    /// Access the task table directly.
63    pub fn task_table(&self) -> &TaskTable {
64        &self.task_table
65    }
66
67    /// Number of tasks in the workflow.
68    pub fn task_count(&self) -> usize {
69        self.task_table.len()
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::ast::analyzed::AnalyzedWorkflow;
77
78    fn make_workflow(
79        tasks: &[&str],
80        provider: Option<&str>,
81        model: Option<&str>,
82    ) -> AnalyzedWorkflow {
83        let mut wf = AnalyzedWorkflow::default();
84        for name in tasks {
85            wf.task_table.insert(name);
86        }
87        wf.provider = provider.map(String::from);
88        wf.model = model.map(String::from);
89        wf
90    }
91
92    #[test]
93    fn from_workflow_roundtrip() {
94        let wf = make_workflow(
95            &["step1", "step2", "step3"],
96            Some("anthropic"),
97            Some("claude-sonnet-4-6"),
98        );
99        let ctx = WorkflowMeta::from_workflow(&wf);
100
101        assert_eq!(ctx.task_count(), 3);
102        assert_eq!(ctx.provider(), Some("anthropic"));
103        assert_eq!(ctx.model(), Some("claude-sonnet-4-6"));
104    }
105
106    #[test]
107    fn task_name_roundtrip() {
108        let wf = make_workflow(&["alpha", "beta"], None, None);
109        let ctx = WorkflowMeta::from_workflow(&wf);
110
111        let id_alpha = ctx.task_id("alpha").unwrap();
112        let id_beta = ctx.task_id("beta").unwrap();
113
114        assert_eq!(ctx.task_name(id_alpha).unwrap(), "alpha");
115        assert_eq!(ctx.task_name(id_beta).unwrap(), "beta");
116    }
117
118    #[test]
119    fn unknown_name_returns_none() {
120        let wf = make_workflow(&["task1"], None, None);
121        let ctx = WorkflowMeta::from_workflow(&wf);
122
123        assert!(ctx.task_id("nonexistent").is_none());
124    }
125
126    #[test]
127    fn provider_and_model_none() {
128        let wf = make_workflow(&["task1"], None, None);
129        let ctx = WorkflowMeta::from_workflow(&wf);
130
131        assert!(ctx.provider().is_none());
132        assert!(ctx.model().is_none());
133    }
134
135    #[test]
136    fn task_table_accessible() {
137        let wf = make_workflow(&["a", "b", "c"], None, None);
138        let ctx = WorkflowMeta::from_workflow(&wf);
139
140        let table = ctx.task_table();
141        assert_eq!(table.len(), 3);
142        assert!(table.contains("a"));
143        assert!(table.contains("b"));
144        assert!(table.contains("c"));
145    }
146
147    #[test]
148    fn arc_sharing() {
149        let wf = make_workflow(&["task1"], Some("openai"), None);
150        let ctx = WorkflowMeta::from_workflow(&wf);
151
152        let ctx2 = Arc::clone(&ctx);
153        assert_eq!(ctx2.task_name(TaskId::new(0)).unwrap(), "task1");
154        assert_eq!(ctx2.provider(), Some("openai"));
155    }
156}