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
11fn 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
23pub(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
34pub(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
47fn 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#[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
106pub fn prepare_create(
110 name: Option<&str>,
111 output: Option<&str>,
112 pattern: Option<&str>,
113) -> Result<CreateParams, ZigError> {
114 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
165pub 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", ¶ms.initial_prompt])
180 .args(["--system-prompt", ¶ms.system_prompt])
181 .args(["--name", ¶ms.session_name])
182 .args(["--tag", ¶ms.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 if Path::new(¶ms.output_path).exists() {
192 match crate::workflow::parser::parse_file(Path::new(¶ms.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;