helix_core/
ast.rs

1use std::collections::HashMap;
2use crate::types::{Value, Duration, TimeUnit};
3#[derive(Debug, Clone)]
4pub struct HelixAst {
5    pub declarations: Vec<Declaration>,
6}
7#[derive(Debug, Clone)]
8pub enum Declaration {
9    Project(ProjectDecl),
10    Agent(AgentDecl),
11    Workflow(WorkflowDecl),
12    Memory(MemoryDecl),
13    Context(ContextDecl),
14    Crew(CrewDecl),
15    Pipeline(PipelineDecl),
16    Plugin(PluginDecl),
17    Database(DatabaseDecl),
18    Load(LoadDecl),
19}
20#[derive(Debug, Clone)]
21pub struct ProjectDecl {
22    pub name: String,
23    pub properties: HashMap<String, Expression>,
24}
25#[derive(Debug, Clone)]
26pub struct AgentDecl {
27    pub name: String,
28    pub properties: HashMap<String, Expression>,
29    pub capabilities: Option<Vec<String>>,
30    pub backstory: Option<BackstoryBlock>,
31    pub tools: Option<Vec<String>>,
32}
33#[derive(Debug, Clone)]
34pub struct WorkflowDecl {
35    pub name: String,
36    pub trigger: Option<Expression>,
37    pub steps: Vec<StepDecl>,
38    pub pipeline: Option<PipelineDecl>,
39    pub properties: HashMap<String, Expression>,
40}
41#[derive(Debug, Clone)]
42pub struct StepDecl {
43    pub name: String,
44    pub agent: Option<String>,
45    pub crew: Option<Vec<String>>,
46    pub task: Option<String>,
47    pub properties: HashMap<String, Expression>,
48}
49#[derive(Debug, Clone)]
50pub struct PipelineDecl {
51    pub flow: Vec<PipelineNode>,
52}
53#[derive(Debug, Clone)]
54pub enum PipelineNode {
55    Step(String),
56    Parallel(Vec<PipelineNode>),
57    Conditional {
58        condition: Expression,
59        then_branch: Box<PipelineNode>,
60        else_branch: Option<Box<PipelineNode>>,
61    },
62}
63#[derive(Debug, Clone)]
64pub struct MemoryDecl {
65    pub provider: String,
66    pub connection: String,
67    pub embeddings: Option<EmbeddingsDecl>,
68    pub properties: HashMap<String, Expression>,
69}
70#[derive(Debug, Clone)]
71pub struct EmbeddingsDecl {
72    pub model: String,
73    pub dimensions: u32,
74    pub properties: HashMap<String, Expression>,
75}
76#[derive(Debug, Clone)]
77pub struct ContextDecl {
78    pub name: String,
79    pub environment: String,
80    pub secrets: Option<HashMap<String, SecretRef>>,
81    pub variables: Option<HashMap<String, Expression>>,
82    pub properties: HashMap<String, Expression>,
83}
84#[derive(Debug, Clone)]
85pub struct CrewDecl {
86    pub name: String,
87    pub agents: Vec<String>,
88    pub process_type: Option<String>,
89    pub properties: HashMap<String, Expression>,
90}
91#[derive(Debug, Clone)]
92pub struct PluginDecl {
93    pub name: String,
94    pub source: String,
95    pub version: Option<String>,
96    pub config: HashMap<String, Expression>,
97}
98#[derive(Debug, Clone)]
99pub struct DatabaseDecl {
100    pub name: String,
101    pub path: Option<String>,
102    pub shards: Option<i64>,
103    pub compression: Option<bool>,
104    pub cache_size: Option<i64>,
105    pub vector_index: Option<VectorIndexConfig>,
106    pub properties: HashMap<String, Expression>,
107}
108#[derive(Debug, Clone)]
109pub struct VectorIndexConfig {
110    pub index_type: String,
111    pub dimensions: i64,
112    pub m: Option<i64>,
113    pub ef_construction: Option<i64>,
114    pub distance_metric: Option<String>,
115    pub properties: HashMap<String, Expression>,
116}
117#[derive(Debug, Clone)]
118pub struct LoadDecl {
119    pub file_name: String,
120    pub properties: HashMap<String, Expression>,
121}
122#[derive(Debug, Clone)]
123pub struct BackstoryBlock {
124    pub lines: Vec<String>,
125}
126#[derive(Debug, Clone)]
127pub enum SecretRef {
128    Environment(String),
129    Vault(String),
130    File(String),
131}
132#[derive(Debug, Clone)]
133pub enum Expression {
134    String(String),
135    Number(f64),
136    Bool(bool),
137    Duration(Duration),
138    Array(Vec<Expression>),
139    Object(HashMap<String, Expression>),
140    Variable(String),
141    Reference(String),
142    IndexedReference(String, String),
143    Identifier(String),
144    Pipeline(Vec<String>),
145    Block(Vec<Statement>),
146    TextBlock(Vec<String>),
147}
148#[derive(Debug, Clone)]
149pub enum Statement {
150    Assignment(String, Expression),
151    Declaration(Declaration),
152    Expression(Expression),
153}
154impl HelixAst {
155    pub fn new() -> Self {
156        HelixAst {
157            declarations: Vec::new(),
158        }
159    }
160    pub fn add_declaration(&mut self, decl: Declaration) {
161        self.declarations.push(decl);
162    }
163    pub fn get_projects(&self) -> Vec<&ProjectDecl> {
164        self.declarations
165            .iter()
166            .filter_map(|d| {
167                if let Declaration::Project(p) = d { Some(p) } else { None }
168            })
169            .collect()
170    }
171    pub fn get_agents(&self) -> Vec<&AgentDecl> {
172        self.declarations
173            .iter()
174            .filter_map(|d| {
175                if let Declaration::Agent(a) = d { Some(a) } else { None }
176            })
177            .collect()
178    }
179    pub fn get_workflows(&self) -> Vec<&WorkflowDecl> {
180        self.declarations
181            .iter()
182            .filter_map(|d| {
183                if let Declaration::Workflow(w) = d { Some(w) } else { None }
184            })
185            .collect()
186    }
187    pub fn get_contexts(&self) -> Vec<&ContextDecl> {
188        self.declarations
189            .iter()
190            .filter_map(|d| {
191                if let Declaration::Context(c) = d { Some(c) } else { None }
192            })
193            .collect()
194    }
195}
196#[allow(dead_code)]
197pub trait AstVisitor {
198    type Result;
199    fn visit_ast(&mut self, ast: &HelixAst) -> Self::Result;
200    fn visit_declaration(&mut self, decl: &Declaration) -> Self::Result;
201    fn visit_project(&mut self, project: &ProjectDecl) -> Self::Result;
202    fn visit_agent(&mut self, agent: &AgentDecl) -> Self::Result;
203    fn visit_workflow(&mut self, workflow: &WorkflowDecl) -> Self::Result;
204    fn visit_memory(&mut self, memory: &MemoryDecl) -> Self::Result;
205    fn visit_context(&mut self, context: &ContextDecl) -> Self::Result;
206    fn visit_crew(&mut self, crew: &CrewDecl) -> Self::Result;
207    fn visit_expression(&mut self, expr: &Expression) -> Self::Result;
208}
209pub struct AstPrettyPrinter {
210    indent: usize,
211    indent_str: String,
212}
213impl AstPrettyPrinter {
214    pub fn new() -> Self {
215        AstPrettyPrinter {
216            indent: 0,
217            indent_str: "  ".to_string(),
218        }
219    }
220    fn write_indent(&self) -> String {
221        self.indent_str.repeat(self.indent)
222    }
223    pub fn print(&mut self, ast: &HelixAst) -> String {
224        let mut result = String::new();
225        result.push_str("# HELIX Language AST\n\n");
226        for decl in &ast.declarations {
227            result.push_str(&self.print_declaration(decl));
228            result.push_str("\n");
229        }
230        result
231    }
232    fn print_declaration(&mut self, decl: &Declaration) -> String {
233        match decl {
234            Declaration::Project(p) => self.print_project(p),
235            Declaration::Agent(a) => self.print_agent(a),
236            Declaration::Workflow(w) => self.print_workflow(w),
237            Declaration::Memory(m) => self.print_memory(m),
238            Declaration::Context(c) => self.print_context(c),
239            Declaration::Crew(cr) => self.print_crew(cr),
240            Declaration::Pipeline(p) => self.print_pipeline(p),
241            Declaration::Plugin(p) => self.print_plugin(p),
242            Declaration::Database(d) => self.print_database(d),
243            Declaration::Load(l) => self.print_load(l),
244        }
245    }
246    fn print_project(&mut self, project: &ProjectDecl) -> String {
247        let mut result = format!(
248            "{}project \"{}\" {{\n", self.write_indent(), project.name
249        );
250        self.indent += 1;
251        for (key, value) in &project.properties {
252            result
253                .push_str(
254                    &format!(
255                        "{}{} = {}\n", self.write_indent(), key, self
256                        .print_expression(value)
257                    ),
258                );
259        }
260        self.indent -= 1;
261        result.push_str(&format!("{}}}\n", self.write_indent()));
262        result
263    }
264    fn print_agent(&mut self, agent: &AgentDecl) -> String {
265        let mut result = format!("{}agent \"{}\" {{\n", self.write_indent(), agent.name);
266        self.indent += 1;
267        for (key, value) in &agent.properties {
268            result
269                .push_str(
270                    &format!(
271                        "{}{} = {}\n", self.write_indent(), key, self
272                        .print_expression(value)
273                    ),
274                );
275        }
276        if let Some(capabilities) = &agent.capabilities {
277            result.push_str(&format!("{}capabilities [\n", self.write_indent()));
278            self.indent += 1;
279            for cap in capabilities {
280                result.push_str(&format!("{}\"{}\"\n", self.write_indent(), cap));
281            }
282            self.indent -= 1;
283            result.push_str(&format!("{}]\n", self.write_indent()));
284        }
285        if let Some(backstory) = &agent.backstory {
286            result.push_str(&format!("{}backstory {{\n", self.write_indent()));
287            self.indent += 1;
288            for line in &backstory.lines {
289                result.push_str(&format!("{}{}\n", self.write_indent(), line));
290            }
291            self.indent -= 1;
292            result.push_str(&format!("{}}}\n", self.write_indent()));
293        }
294        self.indent -= 1;
295        result.push_str(&format!("{}}}\n", self.write_indent()));
296        result
297    }
298    fn print_workflow(&mut self, workflow: &WorkflowDecl) -> String {
299        let mut result = format!(
300            "{}workflow \"{}\" {{\n", self.write_indent(), workflow.name
301        );
302        self.indent += 1;
303        if let Some(trigger) = &workflow.trigger {
304            result
305                .push_str(
306                    &format!(
307                        "{}trigger = {}\n", self.write_indent(), self
308                        .print_expression(trigger)
309                    ),
310                );
311        }
312        for step in &workflow.steps {
313            result.push_str(&self.print_step(step));
314        }
315        if let Some(pipeline) = &workflow.pipeline {
316            result.push_str(&self.print_pipeline(pipeline));
317        }
318        for (key, value) in &workflow.properties {
319            result
320                .push_str(
321                    &format!(
322                        "{}{} = {}\n", self.write_indent(), key, self
323                        .print_expression(value)
324                    ),
325                );
326        }
327        self.indent -= 1;
328        result.push_str(&format!("{}}}\n", self.write_indent()));
329        result
330    }
331    fn print_step(&mut self, step: &StepDecl) -> String {
332        let mut result = format!("{}step \"{}\" {{\n", self.write_indent(), step.name);
333        self.indent += 1;
334        if let Some(agent) = &step.agent {
335            result.push_str(&format!("{}agent = \"{}\"\n", self.write_indent(), agent));
336        }
337        if let Some(crew) = &step.crew {
338            result.push_str(&format!("{}crew = [", self.write_indent()));
339            result
340                .push_str(
341                    &crew
342                        .iter()
343                        .map(|c| format!("\"{}\"", c))
344                        .collect::<Vec<_>>()
345                        .join(", "),
346                );
347            result.push_str("]\n");
348        }
349        if let Some(task) = &step.task {
350            result.push_str(&format!("{}task = \"{}\"\n", self.write_indent(), task));
351        }
352        for (key, value) in &step.properties {
353            result
354                .push_str(
355                    &format!(
356                        "{}{} = {}\n", self.write_indent(), key, self
357                        .print_expression(value)
358                    ),
359                );
360        }
361        self.indent -= 1;
362        result.push_str(&format!("{}}}\n", self.write_indent()));
363        result
364    }
365    fn print_memory(&mut self, memory: &MemoryDecl) -> String {
366        let mut result = format!("{}memory {{\n", self.write_indent());
367        self.indent += 1;
368        result
369            .push_str(
370                &format!("{}provider = \"{}\"\n", self.write_indent(), memory.provider),
371            );
372        result
373            .push_str(
374                &format!(
375                    "{}connection = \"{}\"\n", self.write_indent(), memory.connection
376                ),
377            );
378        if let Some(embeddings) = &memory.embeddings {
379            result.push_str(&self.print_embeddings(embeddings));
380        }
381        for (key, value) in &memory.properties {
382            result
383                .push_str(
384                    &format!(
385                        "{}{} = {}\n", self.write_indent(), key, self
386                        .print_expression(value)
387                    ),
388                );
389        }
390        self.indent -= 1;
391        result.push_str(&format!("{}}}\n", self.write_indent()));
392        result
393    }
394    fn print_embeddings(&mut self, embeddings: &EmbeddingsDecl) -> String {
395        let mut result = format!("{}embeddings {{\n", self.write_indent());
396        self.indent += 1;
397        result
398            .push_str(
399                &format!("{}model = \"{}\"\n", self.write_indent(), embeddings.model),
400            );
401        result
402            .push_str(
403                &format!(
404                    "{}dimensions = {}\n", self.write_indent(), embeddings.dimensions
405                ),
406            );
407        for (key, value) in &embeddings.properties {
408            result
409                .push_str(
410                    &format!(
411                        "{}{} = {}\n", self.write_indent(), key, self
412                        .print_expression(value)
413                    ),
414                );
415        }
416        self.indent -= 1;
417        result.push_str(&format!("{}}}\n", self.write_indent()));
418        result
419    }
420    fn print_context(&mut self, context: &ContextDecl) -> String {
421        let mut result = format!(
422            "{}context \"{}\" {{\n", self.write_indent(), context.name
423        );
424        self.indent += 1;
425        result
426            .push_str(
427                &format!(
428                    "{}environment = \"{}\"\n", self.write_indent(), context.environment
429                ),
430            );
431        if let Some(secrets) = &context.secrets {
432            result.push_str(&format!("{}secrets {{\n", self.write_indent()));
433            self.indent += 1;
434            for (key, secret_ref) in secrets {
435                result
436                    .push_str(
437                        &format!(
438                            "{}{} = {}\n", self.write_indent(), key, self
439                            .print_secret_ref(secret_ref)
440                        ),
441                    );
442            }
443            self.indent -= 1;
444            result.push_str(&format!("{}}}\n", self.write_indent()));
445        }
446        if let Some(variables) = &context.variables {
447            result.push_str(&format!("{}variables {{\n", self.write_indent()));
448            self.indent += 1;
449            for (key, value) in variables {
450                result
451                    .push_str(
452                        &format!(
453                            "{}{} = {}\n", self.write_indent(), key, self
454                            .print_expression(value)
455                        ),
456                    );
457            }
458            self.indent -= 1;
459            result.push_str(&format!("{}}}\n", self.write_indent()));
460        }
461        for (key, value) in &context.properties {
462            result
463                .push_str(
464                    &format!(
465                        "{}{} = {}\n", self.write_indent(), key, self
466                        .print_expression(value)
467                    ),
468                );
469        }
470        self.indent -= 1;
471        result.push_str(&format!("{}}}\n", self.write_indent()));
472        result
473    }
474    fn print_crew(&mut self, crew: &CrewDecl) -> String {
475        let mut result = format!("{}crew \"{}\" {{\n", self.write_indent(), crew.name);
476        self.indent += 1;
477        result.push_str(&format!("{}agents [\n", self.write_indent()));
478        self.indent += 1;
479        for agent in &crew.agents {
480            result.push_str(&format!("{}\"{}\"\n", self.write_indent(), agent));
481        }
482        self.indent -= 1;
483        result.push_str(&format!("{}]\n", self.write_indent()));
484        if let Some(process_type) = &crew.process_type {
485            result
486                .push_str(
487                    &format!("{}process = \"{}\"\n", self.write_indent(), process_type),
488                );
489        }
490        for (key, value) in &crew.properties {
491            result
492                .push_str(
493                    &format!(
494                        "{}{} = {}\n", self.write_indent(), key, self
495                        .print_expression(value)
496                    ),
497                );
498        }
499        self.indent -= 1;
500        result.push_str(&format!("{}}}\n", self.write_indent()));
501        result
502    }
503    fn print_plugin(&mut self, plugin: &PluginDecl) -> String {
504        let mut result = format!(
505            "{}plugin \"{}\" {{\n", self.write_indent(), plugin.name
506        );
507        self.indent += 1;
508        result
509            .push_str(
510                &format!("{}source = \"{}\"\n", self.write_indent(), plugin.source),
511            );
512        if let Some(version) = &plugin.version {
513            result
514                .push_str(
515                    &format!("{}version = \"{}\"\n", self.write_indent(), version),
516                );
517        }
518        for (key, value) in &plugin.config {
519            result
520                .push_str(
521                    &format!(
522                        "{}{} = {}\n", self.write_indent(), key, self
523                        .print_expression(value)
524                    ),
525                );
526        }
527        self.indent -= 1;
528        result.push_str(&format!("{}}}\n", self.write_indent()));
529        result
530    }
531    fn print_database(&mut self, database: &DatabaseDecl) -> String {
532        let mut result = format!(
533            "{}database \"{}\" {{\n", self.write_indent(), database.name
534        );
535        self.indent += 1;
536        if let Some(path) = &database.path {
537            result.push_str(&format!("{}path = \"{}\"\n", self.write_indent(), path));
538        }
539        if let Some(shards) = database.shards {
540            result.push_str(&format!("{}shards = {}\n", self.write_indent(), shards));
541        }
542        if let Some(compression) = database.compression {
543            result
544                .push_str(
545                    &format!("{}compression = {}\n", self.write_indent(), compression),
546                );
547        }
548        if let Some(cache_size) = database.cache_size {
549            result
550                .push_str(
551                    &format!("{}cache_size = {}\n", self.write_indent(), cache_size),
552                );
553        }
554        if let Some(vector_index) = &database.vector_index {
555            result.push_str(&format!("{}vector_index {{\n", self.write_indent()));
556            self.indent += 1;
557            result
558                .push_str(
559                    &format!(
560                        "{}index_type = \"{}\"\n", self.write_indent(), vector_index
561                        .index_type
562                    ),
563                );
564            result
565                .push_str(
566                    &format!(
567                        "{}dimensions = {}\n", self.write_indent(), vector_index
568                        .dimensions
569                    ),
570                );
571            if let Some(m) = vector_index.m {
572                result.push_str(&format!("{}m = {}\n", self.write_indent(), m));
573            }
574            if let Some(ef_construction) = vector_index.ef_construction {
575                result
576                    .push_str(
577                        &format!(
578                            "{}ef_construction = {}\n", self.write_indent(),
579                            ef_construction
580                        ),
581                    );
582            }
583            if let Some(distance_metric) = &vector_index.distance_metric {
584                result
585                    .push_str(
586                        &format!(
587                            "{}distance_metric = \"{}\"\n", self.write_indent(),
588                            distance_metric
589                        ),
590                    );
591            }
592            self.indent -= 1;
593            result.push_str(&format!("{}}}\n", self.write_indent()));
594        }
595        for (key, value) in &database.properties {
596            result
597                .push_str(
598                    &format!(
599                        "{}{} = {}\n", self.write_indent(), key, self
600                        .print_expression(value)
601                    ),
602                );
603        }
604        self.indent -= 1;
605        result.push_str(&format!("{}}}\n", self.write_indent()));
606        result
607    }
608    fn print_load(&mut self, load: &LoadDecl) -> String {
609        let mut result = format!(
610            "{}load \"{}\" {{\n", self.write_indent(), load.file_name
611        );
612        self.indent += 1;
613        for (key, value) in &load.properties {
614            result
615                .push_str(
616                    &format!(
617                        "{}{} = {}\n", self.write_indent(), key, self
618                        .print_expression(value)
619                    ),
620                );
621        }
622        self.indent -= 1;
623        result.push_str(&format!("{}}}\n", self.write_indent()));
624        result
625    }
626    fn print_pipeline(&mut self, pipeline: &PipelineDecl) -> String {
627        let mut result = format!("{}pipeline {{\n", self.write_indent());
628        self.indent += 1;
629        let flow_str = pipeline
630            .flow
631            .iter()
632            .map(|node| match node {
633                PipelineNode::Step(s) => s.clone(),
634                _ => "...".to_string(),
635            })
636            .collect::<Vec<_>>()
637            .join(" -> ");
638        result.push_str(&format!("{}{}\n", self.write_indent(), flow_str));
639        self.indent -= 1;
640        result.push_str(&format!("{}}}\n", self.write_indent()));
641        result
642    }
643    fn print_secret_ref(&mut self, secret_ref: &SecretRef) -> String {
644        match secret_ref {
645            SecretRef::Environment(var) => format!("${}", var),
646            SecretRef::Vault(path) => format!("vault:\"{}\"", path),
647            SecretRef::File(path) => format!("file:\"{}\"", path),
648        }
649    }
650    fn print_expression(&mut self, expr: &Expression) -> String {
651        match expr {
652            Expression::String(s) => format!("\"{}\"", s),
653            Expression::Number(n) => format!("{}", n),
654            Expression::Bool(b) => format!("{}", b),
655            Expression::Duration(d) => {
656                format!(
657                    "{}{}", d.value, match d.unit { TimeUnit::Seconds => "s",
658                    TimeUnit::Minutes => "m", TimeUnit::Hours => "h", TimeUnit::Days =>
659                    "d", }
660                )
661            }
662            Expression::Variable(v) => format!("${}", v),
663            Expression::Reference(r) => format!("@{}", r),
664            Expression::IndexedReference(file, key) => format!("@{}[{}]", file, key),
665            Expression::Identifier(i) => i.clone(),
666            Expression::Pipeline(stages) => stages.join(" -> "),
667            Expression::Array(items) => {
668                format!(
669                    "[{}]", items.iter().map(| i | self.print_expression(i)).collect::<
670                    Vec < _ >> ().join(", ")
671                )
672            }
673            Expression::Object(map) => {
674                let items = map
675                    .iter()
676                    .map(|(k, v)| format!("{} = {}", k, self.print_expression(v)))
677                    .collect::<Vec<_>>()
678                    .join(", ");
679                format!("{{ {} }}", items)
680            }
681            _ => "...".to_string(),
682        }
683    }
684}
685impl Expression {
686    pub fn as_string(&self) -> Option<String> {
687        match self {
688            Expression::String(s) => Some(s.clone()),
689            Expression::Identifier(s) => Some(s.clone()),
690            _ => None,
691        }
692    }
693    pub fn as_number(&self) -> Option<f64> {
694        match self {
695            Expression::Number(n) => Some(*n),
696            _ => None,
697        }
698    }
699    pub fn as_bool(&self) -> Option<bool> {
700        match self {
701            Expression::Bool(b) => Some(*b),
702            _ => None,
703        }
704    }
705    pub fn as_array(&self) -> Option<Vec<Expression>> {
706        match self {
707            Expression::Array(arr) => Some(arr.clone()),
708            _ => None,
709        }
710    }
711    pub fn as_object(&self) -> Option<&HashMap<String, Expression>> {
712        match self {
713            Expression::Object(map) => Some(map),
714            _ => None,
715        }
716    }
717    pub fn to_value(&self) -> Value {
718        match self {
719            Expression::String(s) => Value::String(s.clone()),
720            Expression::Number(n) => Value::Number(*n),
721            Expression::Bool(b) => Value::Bool(*b),
722            Expression::Duration(d) => Value::Duration(d.clone()),
723            Expression::Array(arr) => {
724                Value::Array(arr.iter().map(|e| e.to_value()).collect())
725            }
726            Expression::Object(map) => {
727                Value::Object(
728                    map.iter().map(|(k, v)| (k.clone(), v.to_value())).collect(),
729                )
730            }
731            Expression::Variable(v) => Value::Reference(format!("${}", v)),
732            Expression::Reference(r) => Value::Reference(format!("@{}", r)),
733            Expression::IndexedReference(file, key) => {
734                Value::Reference(format!("@{}[{}]", file, key))
735            }
736            Expression::Identifier(i) => Value::String(i.clone()),
737            _ => Value::String("".to_string()),
738        }
739    }
740}
741#[allow(dead_code)]
742pub struct AstBuilder {
743    ast: HelixAst,
744}
745#[allow(dead_code)]
746impl AstBuilder {
747    pub fn new() -> Self {
748        AstBuilder { ast: HelixAst::new() }
749    }
750    pub fn add_project(
751        mut self,
752        name: String,
753        properties: HashMap<String, Expression>,
754    ) -> Self {
755        self.ast.add_declaration(Declaration::Project(ProjectDecl { name, properties }));
756        self
757    }
758    pub fn add_agent(mut self, agent: AgentDecl) -> Self {
759        self.ast.add_declaration(Declaration::Agent(agent));
760        self
761    }
762    pub fn add_workflow(mut self, workflow: WorkflowDecl) -> Self {
763        self.ast.add_declaration(Declaration::Workflow(workflow));
764        self
765    }
766    pub fn add_context(mut self, context: ContextDecl) -> Self {
767        self.ast.add_declaration(Declaration::Context(context));
768        self
769    }
770    pub fn build(self) -> HelixAst {
771        self.ast
772    }
773}