Skip to main content

zig_core/
create.rs

1use std::collections::HashMap;
2use std::path::Path;
3use std::process::Command;
4
5use serde::Serialize;
6
7use crate::error::ZigError;
8use crate::prompt;
9use crate::run;
10
11/// Build the system prompt for the create agent by rendering the template
12/// with injected variables.
13fn build_system_prompt(zag_help: &str, zag_orch: &str) -> String {
14    let vars = HashMap::from([
15        ("zug_format_spec", prompt::templates::CONFIG_SIDECAR),
16        ("zag_help", zag_help),
17        ("zag_orch", zag_orch),
18    ]);
19    prompt::render(prompt::templates::CREATE, &vars)
20}
21
22/// Attempt to capture zag CLI reference text via `zag --help-agent`.
23fn get_zag_help() -> String {
24    Command::new("zag")
25        .arg("--help-agent")
26        .output()
27        .ok()
28        .filter(|o| o.status.success())
29        .and_then(|o| String::from_utf8(o.stdout).ok())
30        .unwrap_or_else(|| "(zag --help-agent not available — zag may not be installed)".into())
31}
32
33/// Attempt to capture zag orchestration reference via `zag man orchestration`.
34fn get_zag_orch_reference() -> String {
35    Command::new("zag")
36        .args(["man", "orchestration"])
37        .output()
38        .ok()
39        .filter(|o| o.status.success())
40        .and_then(|o| String::from_utf8(o.stdout).ok())
41        .unwrap_or_else(|| {
42            "(zag man orchestration not available — zag may not be installed)".into()
43        })
44}
45
46/// Return pattern-specific guidance sentences to inject into the initial prompt.
47fn pattern_guidance(pattern: &str) -> &'static str {
48    match pattern {
49        "sequential" => {
50            "\
51            Use the Sequential Pipeline pattern: steps run in order, each feeding \
52            its output to the next via inject_context. Structure it as a linear \
53            depends_on chain."
54        }
55        "fan-out" => {
56            "\
57            Use the Fan-Out / Gather pattern: multiple independent steps run in \
58            parallel and a final step synthesizes their results. The gathering step \
59            should depends_on all parallel steps with inject_context = true."
60        }
61        "generator-critic" => {
62            "\
63            Use the Generator / Critic pattern: a generation step is followed by a \
64            critique step that scores the output, looping back via the next field \
65            until a quality threshold is met. Use saves to extract score and \
66            feedback variables, and a condition to control the loop."
67        }
68        "coordinator-dispatcher" => {
69            "\
70            Use the Coordinator / Dispatcher pattern: an initial classification \
71            step routes to specialized handler steps via condition expressions. \
72            Use json + saves to extract the classification, then condition guards \
73            on each handler step."
74        }
75        "hierarchical-decomposition" => {
76            "\
77            Use the Hierarchical Decomposition pattern: a planning step breaks the \
78            problem into sub-tasks, multiple worker steps handle each sub-task in \
79            parallel, and a synthesis step combines the results."
80        }
81        "human-in-the-loop" => {
82            "\
83            Use the Human-in-the-Loop pattern: incorporate human approval gates \
84            between automated steps. Use condition expressions on approval \
85            variables that are set by human review."
86        }
87        "inter-agent-communication" => {
88            "\
89            Use the Inter-Agent Communication pattern: agents collaborate via \
90            shared variables. Design the vars section carefully so agents can \
91            read and write to common state."
92        }
93        _ => "",
94    }
95}
96
97/// Prepared parameters for workflow creation — used by both the CLI
98/// (which spawns zag directly) and the API (which returns data to the frontend).
99#[derive(Debug, Clone, Serialize)]
100pub struct CreateParams {
101    pub system_prompt: String,
102    pub initial_prompt: String,
103    pub output_path: String,
104    pub session_name: String,
105    pub session_tag: String,
106}
107
108/// Prepare the prompts and configuration for workflow creation without
109/// launching zag. Returns structured data that can be used by the CLI
110/// to spawn zag or by the API to return to the frontend.
111pub fn prepare_create(
112    name: Option<&str>,
113    output: Option<&str>,
114    pattern: Option<&str>,
115) -> Result<CreateParams, ZigError> {
116    let zag_help = get_zag_help();
117    let zag_orch = get_zag_orch_reference();
118    let system_prompt = build_system_prompt(&zag_help, &zag_orch);
119
120    let output_path = if let Some(o) = output {
121        o.to_string()
122    } else {
123        let global_dir = crate::paths::ensure_global_workflows_dir()?;
124        let filename = name
125            .map(|n| format!("{n}.zug"))
126            .unwrap_or_else(|| "workflow.zug".to_string());
127        global_dir.join(&filename).to_string_lossy().to_string()
128    };
129
130    let mut initial_prompt = if let Some(n) = name {
131        format!(
132            "I want to create a workflow called \"{n}\". \
133             The output will be saved to {output_path}. \
134             Please help me design it — start by asking what process I want to automate."
135        )
136    } else {
137        format!(
138            "I want to create a new workflow. \
139             The output will be saved to {output_path}. \
140             Please help me design it — start by asking what process I want to automate."
141        )
142    };
143
144    if let Some(p) = pattern {
145        let guidance = pattern_guidance(p);
146        if !guidance.is_empty() {
147            initial_prompt.push(' ');
148            initial_prompt.push_str(guidance);
149        }
150    }
151
152    Ok(CreateParams {
153        system_prompt,
154        initial_prompt,
155        output_path,
156        session_name: "zig-create".to_string(),
157        session_tag: "zig-workflow-creation".to_string(),
158    })
159}
160
161/// Launch an interactive zag session for workflow creation.
162///
163/// The agent is given full context about zag and the .zug format, and guides
164/// the user through designing their workflow.
165pub fn run_create(
166    name: Option<&str>,
167    output: Option<&str>,
168    pattern: Option<&str>,
169) -> Result<(), ZigError> {
170    run::check_zag()?;
171
172    let params = prepare_create(name, output, pattern)?;
173
174    let status = Command::new("zag")
175        .args(["run", &params.initial_prompt])
176        .args(["--system-prompt", &params.system_prompt])
177        .args(["--name", &params.session_name])
178        .args(["--tag", &params.session_tag])
179        .status()
180        .map_err(|e| ZigError::Zag(format!("failed to launch zag: {e}")))?;
181
182    if !status.success() {
183        return Err(ZigError::Zag(format!("zag exited with status {status}")));
184    }
185
186    // Validate the output if it was created
187    if Path::new(&params.output_path).exists() {
188        match crate::workflow::parser::parse_file(Path::new(&params.output_path)) {
189            Ok(workflow) => {
190                if let Err(errors) = crate::workflow::validate::validate(&workflow) {
191                    eprintln!("Warning: generated workflow has validation issues:");
192                    for e in &errors {
193                        eprintln!("  - {e}");
194                    }
195                }
196            }
197            Err(e) => {
198                eprintln!("Warning: could not parse generated file: {e}");
199            }
200        }
201    }
202
203    Ok(())
204}
205
206#[cfg(test)]
207#[path = "create_tests.rs"]
208mod tests;