1use std::collections::HashMap;
2use std::path::Path;
3
4use serde::Serialize;
5use zag_agent::builder::AgentBuilder;
6use zag_agent::manpages;
7
8use crate::error::ZigError;
9use crate::prompt;
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 {
28 manpages::HELP_AGENT.to_string()
29}
30
31pub(crate) fn get_zag_orch_reference() -> String {
34 manpages::ORCHESTRATION.to_string()
35}
36
37fn pattern_guidance(pattern: &str) -> String {
42 match pattern {
43 "sequential" => {
44 "Use the Sequential Pipeline pattern: steps run in order, each feeding \
45 its output to the next via inject_context. Structure it as a linear \
46 depends_on chain."
47 }
48 "fan-out" => {
49 "Use the Fan-Out / Gather pattern: multiple independent steps run in \
50 parallel and a final step synthesizes their results. The gathering step \
51 should depends_on all parallel steps with inject_context = true."
52 }
53 "generator-critic" => {
54 "Use the Generator / Critic pattern: a generation step is followed by a \
55 critique step that scores the output, looping back via the next field \
56 until a quality threshold is met. Use saves to extract score and \
57 feedback variables, and a condition to control the loop."
58 }
59 "coordinator-dispatcher" => {
60 "Use the Coordinator / Dispatcher pattern: an initial classification \
61 step routes to specialized handler steps via condition expressions. \
62 Use json + saves to extract the classification, then condition guards \
63 on each handler step."
64 }
65 "hierarchical-decomposition" => {
66 "Use the Hierarchical Decomposition pattern: a planning step breaks the \
67 problem into sub-tasks, multiple worker steps handle each sub-task in \
68 parallel, and a synthesis step combines the results."
69 }
70 "human-in-the-loop" => {
71 "Use the Human-in-the-Loop pattern: incorporate human approval gates \
72 between automated steps. Use condition expressions on approval \
73 variables that are set by human review."
74 }
75 "inter-agent-communication" => {
76 "Use the Inter-Agent Communication pattern: agents collaborate via \
77 shared variables. Design the vars section carefully so agents can \
78 read and write to common state."
79 }
80 _ => "",
81 }
82 .to_string()
83}
84
85#[derive(Debug, Clone, Serialize)]
88pub struct CreateParams {
89 pub system_prompt: String,
90 pub initial_prompt: String,
91 pub output_path: String,
92 pub session_name: String,
93 pub session_tag: String,
94}
95
96pub fn prepare_create(
100 name: Option<&str>,
101 output: Option<&str>,
102 pattern: Option<&str>,
103) -> Result<CreateParams, ZigError> {
104 if let Err(e) = prompt::write_examples_to_global_dir() {
106 eprintln!("Warning: could not write example files: {e}");
107 }
108
109 let zag_help = get_zag_help();
110 let zag_orch = get_zag_orch_reference();
111 let examples_reference = prompt::examples_reference_block();
112 let system_prompt = build_system_prompt(&zag_help, &zag_orch, &examples_reference);
113
114 let output_path = if let Some(o) = output {
115 o.to_string()
116 } else {
117 let global_dir = crate::paths::ensure_global_workflows_dir()?;
118 let filename = name
119 .map(|n| format!("{n}.zwf"))
120 .unwrap_or_else(|| "workflow.zwf".to_string());
121 crate::paths::collapse_home(&global_dir.join(&filename).to_string_lossy())
122 };
123
124 let mut initial_prompt = if let Some(n) = name {
125 format!(
126 "I want to create a workflow called \"{n}\". \
127 The output will be saved to {output_path}. \
128 Please help me design it — start by asking what process I want to automate."
129 )
130 } else {
131 format!(
132 "I want to create a new workflow. \
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 };
137
138 if let Some(p) = pattern {
139 let guidance = pattern_guidance(p);
140 if !guidance.is_empty() {
141 initial_prompt.push(' ');
142 initial_prompt.push_str(&guidance);
143 }
144 }
145
146 Ok(CreateParams {
147 system_prompt,
148 initial_prompt,
149 output_path,
150 session_name: "zig-create".to_string(),
151 session_tag: "zig-workflow-creation".to_string(),
152 })
153}
154
155pub async fn run_create(
161 name: Option<&str>,
162 output: Option<&str>,
163 pattern: Option<&str>,
164) -> Result<(), ZigError> {
165 let params = prepare_create(name, output, pattern)?;
166
167 AgentBuilder::new()
168 .system_prompt(¶ms.system_prompt)
169 .name(¶ms.session_name)
170 .tag(¶ms.session_tag)
171 .run(Some(¶ms.initial_prompt))
172 .await
173 .map_err(|e| ZigError::Zag(format!("failed to run agent: {e}")))?;
174
175 if Path::new(¶ms.output_path).exists() {
177 match crate::workflow::parser::parse_file(Path::new(¶ms.output_path)) {
178 Ok(workflow) => {
179 if let Err(errors) = crate::workflow::validate::validate(&workflow) {
180 eprintln!("Warning: generated workflow has validation issues:");
181 for e in &errors {
182 eprintln!(" - {e}");
183 }
184 }
185 }
186 Err(e) => {
187 eprintln!("Warning: could not parse generated file: {e}");
188 }
189 }
190 }
191
192 Ok(())
193}
194
195#[cfg(test)]
196#[path = "create_tests.rs"]
197mod tests;