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