Skip to main content

zig_core/
prompt.rs

1use std::collections::HashMap;
2
3use crate::error::ZigError;
4
5/// Prompt templates are embedded at compile time from `prompts/`.
6///
7/// Markdown prompt files carry YAML front matter (`name`, `version`,
8/// `description`, `references`) that is metadata for humans and tooling. It
9/// must never be sent to agents, so the accessors below strip front matter
10/// before returning the template content.
11pub mod templates {
12    use std::sync::LazyLock;
13
14    /// System prompt for `zig workflow create` — the interactive workflow design agent.
15    pub fn create() -> &'static str {
16        static STRIPPED: LazyLock<&'static str> =
17            LazyLock::new(|| super::strip_front_matter(include_str!("../prompts/create/2_2.md")));
18        *STRIPPED
19    }
20
21    /// System prompt for `zig workflow update` — the interactive workflow revision agent.
22    pub fn update() -> &'static str {
23        static STRIPPED: LazyLock<&'static str> =
24            LazyLock::new(|| super::strip_front_matter(include_str!("../prompts/update/1_2.md")));
25        *STRIPPED
26    }
27
28    /// `.zwf` format specification — injected as a reference sidecar into prompts
29    /// that need to produce or reason about `.zwf` files.
30    pub fn config_sidecar() -> &'static str {
31        static STRIPPED: LazyLock<&'static str> = LazyLock::new(|| {
32            super::strip_front_matter(include_str!("../prompts/config-sidecar/1_4.md"))
33        });
34        *STRIPPED
35    }
36
37    /// Example `.zwf` files for each orchestration pattern — embedded at compile
38    /// time and written to `~/.zig/examples/` during workflow creation or update.
39    pub mod examples {
40        pub const SEQUENTIAL: &str = include_str!("../prompts/examples/sequential.zwf");
41        pub const FAN_OUT: &str = include_str!("../prompts/examples/fan-out.zwf");
42        pub const GENERATOR_CRITIC: &str = include_str!("../prompts/examples/generator-critic.zwf");
43        pub const COORDINATOR_DISPATCHER: &str =
44            include_str!("../prompts/examples/coordinator-dispatcher.zwf");
45        pub const HIERARCHICAL_DECOMPOSITION: &str =
46            include_str!("../prompts/examples/hierarchical-decomposition.zwf");
47        pub const HUMAN_IN_THE_LOOP: &str =
48            include_str!("../prompts/examples/human-in-the-loop.zwf");
49        pub const INTER_AGENT_COMMUNICATION: &str =
50            include_str!("../prompts/examples/inter-agent-communication.zwf");
51    }
52}
53
54/// Short description for each embedded example file, used in the
55/// [`examples_reference_block`] helper.
56const EXAMPLE_DESCRIPTIONS: &[(&str, &str)] = &[
57    (
58        "sequential.zwf",
59        "Sequential Pipeline — blog post workflow (research → draft → edit → SEO)",
60    ),
61    (
62        "fan-out.zwf",
63        "Fan-Out / Gather — PR review with parallel security/performance/design reviewers and synthesis",
64    ),
65    (
66        "generator-critic.zwf",
67        "Generator / Critic — landing page copy with iterative quality scoring loop",
68    ),
69    (
70        "coordinator-dispatcher.zwf",
71        "Coordinator / Dispatcher — support ticket classification routed to specialist handlers",
72    ),
73    (
74        "hierarchical-decomposition.zwf",
75        "Hierarchical Decomposition — feature spec broken into parallel analysis tracks",
76    ),
77    (
78        "human-in-the-loop.zwf",
79        "Human-in-the-Loop — database migration plan with approval gates",
80    ),
81    (
82        "inter-agent-communication.zwf",
83        "Inter-Agent Communication — RFC review with advocate/skeptic/moderator roles",
84    ),
85];
86
87/// Return the embedded example `.zwf` content for a given pattern name.
88pub fn example_for_pattern(pattern: &str) -> Option<&'static str> {
89    match pattern {
90        "sequential" => Some(templates::examples::SEQUENTIAL),
91        "fan-out" => Some(templates::examples::FAN_OUT),
92        "generator-critic" => Some(templates::examples::GENERATOR_CRITIC),
93        "coordinator-dispatcher" => Some(templates::examples::COORDINATOR_DISPATCHER),
94        "hierarchical-decomposition" => Some(templates::examples::HIERARCHICAL_DECOMPOSITION),
95        "human-in-the-loop" => Some(templates::examples::HUMAN_IN_THE_LOOP),
96        "inter-agent-communication" => Some(templates::examples::INTER_AGENT_COMMUNICATION),
97        _ => None,
98    }
99}
100
101/// Return all embedded example files as `(filename, content)` pairs.
102pub fn all_examples() -> Vec<(&'static str, &'static str)> {
103    vec![
104        ("sequential.zwf", templates::examples::SEQUENTIAL),
105        ("fan-out.zwf", templates::examples::FAN_OUT),
106        (
107            "generator-critic.zwf",
108            templates::examples::GENERATOR_CRITIC,
109        ),
110        (
111            "coordinator-dispatcher.zwf",
112            templates::examples::COORDINATOR_DISPATCHER,
113        ),
114        (
115            "hierarchical-decomposition.zwf",
116            templates::examples::HIERARCHICAL_DECOMPOSITION,
117        ),
118        (
119            "human-in-the-loop.zwf",
120            templates::examples::HUMAN_IN_THE_LOOP,
121        ),
122        (
123            "inter-agent-communication.zwf",
124            templates::examples::INTER_AGENT_COMMUNICATION,
125        ),
126    ]
127}
128
129/// Strip YAML front matter from a prompt template.
130///
131/// Front matter is the block at the start of the file delimited by `---` lines:
132///
133/// ```text
134/// ---
135/// name: my-prompt
136/// version: "1.0"
137/// ---
138///
139/// the actual prompt body starts here
140/// ```
141///
142/// The leading delimiter must be on the very first line. If no front matter is
143/// present, the input is returned unchanged. A single blank line immediately
144/// following the closing delimiter is also consumed so the returned content
145/// starts with the prompt body.
146///
147/// Both LF and CRLF line endings are supported so the helper behaves the same
148/// on Unix and Windows checkouts.
149pub fn strip_front_matter(content: &str) -> &str {
150    // Match the opening delimiter line (`---` followed by LF or CRLF).
151    let rest = if let Some(r) = content.strip_prefix("---\n") {
152        r
153    } else if let Some(r) = content.strip_prefix("---\r\n") {
154        r
155    } else {
156        return content;
157    };
158
159    // Scan line-by-line for the closing `---` delimiter so we tolerate either
160    // line ending and a missing trailing newline.
161    let mut offset = 0;
162    while offset <= rest.len() {
163        let remainder = &rest[offset..];
164        let (line, advance) = match remainder.find('\n') {
165            Some(nl) => (&remainder[..nl], nl + 1),
166            None => (remainder, remainder.len()),
167        };
168        let trimmed = line.strip_suffix('\r').unwrap_or(line);
169        if trimmed == "---" {
170            let body_start = offset + advance;
171            let body = &rest[body_start..];
172            // Consume one optional blank line after the closing delimiter so
173            // the returned content starts at the prompt body.
174            return body
175                .strip_prefix("\r\n")
176                .or_else(|| body.strip_prefix('\n'))
177                .unwrap_or(body);
178        }
179        if advance == 0 {
180            break;
181        }
182        offset += advance;
183    }
184
185    // No closing delimiter found — treat as no front matter rather than
186    // swallowing the whole file.
187    content
188}
189
190/// Render a reference block listing the example `.zwf` files that live in
191/// `~/.zig/examples/`. Used as the `{{examples_reference}}` variable in the
192/// `create` and `update` system prompts so the agent knows canonical pattern
193/// examples are available on disk to read from.
194pub fn examples_reference_block() -> String {
195    let mut out = String::new();
196    out.push_str(
197        "Here are examples of agent orchestration patterns, for reference. \
198         Read the relevant file(s) before designing or editing a workflow so \
199         the structure matches a proven pattern:\n\n",
200    );
201    for (filename, description) in EXAMPLE_DESCRIPTIONS {
202        out.push_str("- `~/.zig/examples/");
203        out.push_str(filename);
204        out.push_str("` — ");
205        out.push_str(description);
206        out.push('\n');
207    }
208    out
209}
210
211/// Write all embedded example `.zwf` files to `~/.zig/examples/` so the
212/// agent can read them while designing or revising a workflow.
213pub fn write_examples_to_global_dir() -> Result<(), ZigError> {
214    let dir = crate::paths::ensure_global_examples_dir()?;
215    for (filename, content) in all_examples() {
216        let path = dir.join(filename);
217        std::fs::write(&path, content)
218            .map_err(|e| ZigError::Io(format!("failed to write {}: {e}", path.display())))?;
219    }
220    Ok(())
221}
222
223/// Render a prompt template by replacing `{{variable}}` placeholders with
224/// values from the provided map.
225///
226/// Unknown variables are left as-is so callers can detect missing bindings.
227pub fn render(template: &str, vars: &HashMap<&str, &str>) -> String {
228    let mut result = template.to_string();
229    for (&key, &value) in vars {
230        let placeholder = format!("{{{{{key}}}}}");
231        result = result.replace(&placeholder, value);
232    }
233    result
234}
235
236#[cfg(test)]
237#[path = "prompt_tests.rs"]
238mod tests;