Skip to main content

zig_core/workflow/
model.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5/// A complete workflow definition parsed from a `.zug` file.
6///
7/// A workflow describes a DAG of agent steps with shared variables,
8/// conditional routing, and data flow between steps. It maps directly
9/// to zag orchestration commands at execution time.
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11pub struct Workflow {
12    /// Workflow metadata.
13    pub workflow: WorkflowMeta,
14
15    /// Reusable role definitions that can be referenced by steps.
16    /// Keys are role names; values define the role's system prompt.
17    #[serde(default)]
18    pub roles: HashMap<String, Role>,
19
20    /// Shared variables that flow between steps.
21    /// Keys are variable names; values define type, default, and description.
22    #[serde(default)]
23    pub vars: HashMap<String, Variable>,
24
25    /// Ordered list of workflow steps. Each step maps to a zag agent invocation.
26    #[serde(default, rename = "step")]
27    pub steps: Vec<Step>,
28}
29
30/// Workflow-level metadata.
31#[derive(Debug, Clone, Default, Serialize, Deserialize)]
32pub struct WorkflowMeta {
33    /// Human-readable workflow name (used as filename if not overridden).
34    pub name: String,
35
36    /// Short description of what this workflow does.
37    #[serde(default)]
38    pub description: String,
39
40    /// Tags for discovery and filtering.
41    #[serde(default)]
42    pub tags: Vec<String>,
43
44    /// Workflow version string (e.g., "1.0.0").
45    #[serde(default)]
46    pub version: Option<String>,
47
48    /// Default zag provider for all steps (claude, codex, gemini, copilot, ollama).
49    /// Individual steps can override this with their own `provider` field.
50    #[serde(default)]
51    pub provider: Option<String>,
52
53    /// Default model name or size alias for all steps (small, medium, large, or specific name).
54    /// Individual steps can override this with their own `model` field.
55    #[serde(default)]
56    pub model: Option<String>,
57
58    /// Workflow-level reference files advertised in the system prompt for every step.
59    ///
60    /// Each entry is either a bare path string or a table with `path`, `name`,
61    /// `description`, and `required` fields. Paths are resolved relative to
62    /// the `.zug` file's directory. See [`ResourceSpec`] for the accepted
63    /// shapes.
64    #[serde(default)]
65    pub resources: Vec<ResourceSpec>,
66}
67
68/// A resource entry — a knowledge file the agent is *told about* (not inlined)
69/// via the system prompt, so it can choose to read it with its file tools.
70///
71/// Accepts two TOML shapes for ergonomics:
72///
73/// ```toml
74/// # Short form — just a path
75/// resources = ["./cv.md", "./style-guide.md"]
76///
77/// # Full form — per-resource metadata
78/// [[resource]]
79/// path = "./cv.md"
80/// name = "cv"
81/// description = "Candidate's current CV"
82/// required = true
83/// ```
84#[derive(Debug, Clone, Serialize, Deserialize)]
85#[serde(untagged)]
86pub enum ResourceSpec {
87    /// Bare path form: `"./cv.md"`.
88    Path(String),
89    /// Table form with optional metadata.
90    Detailed {
91        /// Path to the resource file, relative to the `.zug` file's directory.
92        path: String,
93        /// Optional display name. Defaults to the file name if absent.
94        #[serde(default)]
95        name: Option<String>,
96        /// Optional one-line description shown alongside the path in the prompt.
97        #[serde(default)]
98        description: Option<String>,
99        /// If true, execution fails when the file cannot be found. Defaults to false (warn + skip).
100        #[serde(default)]
101        required: bool,
102    },
103}
104
105impl ResourceSpec {
106    /// The raw path string as written in the `.zug` file.
107    pub fn path(&self) -> &str {
108        match self {
109            ResourceSpec::Path(p) => p,
110            ResourceSpec::Detailed { path, .. } => path,
111        }
112    }
113
114    /// The optional explicit display name, if one was set.
115    pub fn name(&self) -> Option<&str> {
116        match self {
117            ResourceSpec::Path(_) => None,
118            ResourceSpec::Detailed { name, .. } => name.as_deref(),
119        }
120    }
121
122    /// The optional description, if one was set.
123    pub fn description(&self) -> Option<&str> {
124        match self {
125            ResourceSpec::Path(_) => None,
126            ResourceSpec::Detailed { description, .. } => description.as_deref(),
127        }
128    }
129
130    /// Whether this resource is required. Bare-path form is never required.
131    pub fn required(&self) -> bool {
132        match self {
133            ResourceSpec::Path(_) => false,
134            ResourceSpec::Detailed { required, .. } => *required,
135        }
136    }
137}
138
139/// A reusable role definition that can be referenced by steps.
140///
141/// Roles define system prompts that shape agent behavior. Each role can
142/// provide its prompt inline or load it from an external file, enabling
143/// maintainable workflows with many distinct personas.
144#[derive(Debug, Clone, Default, Serialize, Deserialize)]
145pub struct Role {
146    /// Inline system prompt for this role. Supports `${var}` references.
147    #[serde(default)]
148    pub system_prompt: Option<String>,
149
150    /// Path to a file containing the system prompt (relative to the .zug file).
151    /// Loaded at execution time. Supports `${var}` references in the file content.
152    #[serde(default)]
153    pub system_prompt_file: Option<String>,
154}
155
156/// A workflow variable — shared state between steps.
157///
158/// Variables can be referenced in step prompts via `${var_name}` and updated
159/// by agents through the zig MCP server during execution.
160#[derive(Debug, Clone, Default, Serialize, Deserialize)]
161pub struct Variable {
162    /// Variable type: "string", "number", "bool", or "json".
163    #[serde(rename = "type")]
164    pub var_type: VarType,
165
166    /// Default value (as a TOML value). If absent, the variable must be
167    /// provided at runtime or set by a preceding step.
168    #[serde(default)]
169    pub default: Option<toml::Value>,
170
171    /// Path to a file whose contents become the default value (relative to .zug file).
172    /// Mutually exclusive with `default`.
173    #[serde(default)]
174    pub default_file: Option<String>,
175
176    /// Human-readable description of this variable's purpose.
177    #[serde(default)]
178    pub description: String,
179
180    // --- Input binding ---
181    /// Bind this variable to an input source. Currently only `"prompt"` is
182    /// supported, which assigns the CLI user prompt to this variable.
183    #[serde(default)]
184    pub from: Option<String>,
185
186    // --- Constraints ---
187    /// If true, the variable must have a non-empty value before execution.
188    #[serde(default)]
189    pub required: bool,
190
191    /// Minimum string length (only valid for `type = "string"`).
192    #[serde(default)]
193    pub min_length: Option<u32>,
194
195    /// Maximum string length (only valid for `type = "string"`).
196    #[serde(default)]
197    pub max_length: Option<u32>,
198
199    /// Minimum numeric value (only valid for `type = "number"`).
200    #[serde(default)]
201    pub min: Option<f64>,
202
203    /// Maximum numeric value (only valid for `type = "number"`).
204    #[serde(default)]
205    pub max: Option<f64>,
206
207    /// Regex pattern the value must match (only valid for `type = "string"`).
208    #[serde(default)]
209    pub pattern: Option<String>,
210
211    /// Restrict value to one of these specific values.
212    #[serde(default)]
213    pub allowed_values: Option<Vec<toml::Value>>,
214}
215
216/// Supported variable types.
217#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
218#[serde(rename_all = "lowercase")]
219pub enum VarType {
220    #[default]
221    String,
222    Number,
223    Bool,
224    Json,
225}
226
227/// A single workflow step — one agent invocation.
228///
229/// Each step maps to a `zag spawn` (or `zag exec` for terminal steps).
230/// Steps form a DAG via `depends_on` and can conditionally execute based
231/// on workflow variable values.
232#[derive(Debug, Clone, Default, Serialize, Deserialize)]
233pub struct Step {
234    /// Unique step identifier (used in `depends_on` references).
235    pub name: String,
236
237    /// Prompt template sent to the agent. May contain `${var_name}` references
238    /// that are resolved against workflow variables before execution.
239    pub prompt: String,
240
241    /// Zag provider to use (claude, codex, gemini, copilot, ollama).
242    /// Falls back to the project/global zag default if not set.
243    #[serde(default)]
244    pub provider: Option<String>,
245
246    /// Model name or size alias (small, medium, large).
247    #[serde(default)]
248    pub model: Option<String>,
249
250    /// Steps that must complete before this step starts.
251    #[serde(default)]
252    pub depends_on: Vec<String>,
253
254    /// If true, dependency outputs are automatically injected into the prompt.
255    #[serde(default)]
256    pub inject_context: bool,
257
258    /// Condition expression that must evaluate to true for this step to run.
259    /// Uses a simple expression language: `var < 8`, `status == "done"`, etc.
260    /// If the condition is false, the step is skipped.
261    #[serde(default)]
262    pub condition: Option<String>,
263
264    /// Request structured JSON output from the agent.
265    #[serde(default)]
266    pub json: bool,
267
268    /// JSON schema to validate agent output against (implies `json = true`).
269    #[serde(default)]
270    pub json_schema: Option<String>,
271
272    /// Output format override: "text", "json", "json-pretty", "stream-json", "native-json".
273    /// When set, maps to `-o <FORMAT>` on zag and overrides the `json` bool field.
274    #[serde(default)]
275    pub output: Option<String>,
276
277    /// Map of variable names to save from this step's output.
278    /// Values are JSONPath-like selectors (e.g., `"$.score"`).
279    /// If the output is plain text, use `"$"` to capture the full output.
280    #[serde(default)]
281    pub saves: HashMap<String, String>,
282
283    /// Step timeout (e.g., "5m", "30s", "1h").
284    #[serde(default)]
285    pub timeout: Option<String>,
286
287    /// Tags applied to the spawned zag session.
288    #[serde(default)]
289    pub tags: Vec<String>,
290
291    /// Behavior on step failure: "fail" (default), "continue", or "retry".
292    #[serde(default)]
293    pub on_failure: Option<FailurePolicy>,
294
295    /// Maximum retry attempts when `on_failure = "retry"`.
296    #[serde(default)]
297    pub max_retries: Option<u32>,
298
299    /// Explicit next step to jump to after completion (enables loops).
300    /// Without this, execution follows the DAG order.
301    #[serde(default)]
302    pub next: Option<String>,
303
304    /// System prompt override for this step's agent.
305    /// Mutually exclusive with `role`.
306    #[serde(default)]
307    pub system_prompt: Option<String>,
308
309    /// Role name or `${var}` reference — resolved to a role from `[roles]` at runtime.
310    /// The role's system prompt is used as this step's system prompt.
311    /// Mutually exclusive with `system_prompt`.
312    #[serde(default)]
313    pub role: Option<String>,
314
315    /// Maximum number of agentic turns for this step.
316    #[serde(default)]
317    pub max_turns: Option<u32>,
318
319    // --- Observability ---
320    /// Human-readable description of this step's purpose.
321    #[serde(default)]
322    pub description: String,
323
324    // --- Execution environment ---
325    /// If true, spawn a long-lived interactive session (FIFO-based).
326    /// Enables Human-in-the-Loop and Inter-Agent Communication patterns.
327    #[serde(default)]
328    pub interactive: bool,
329
330    /// If true, auto-approve all agent actions (skip permission prompts).
331    #[serde(default)]
332    pub auto_approve: bool,
333
334    /// Working directory override for this step's agent.
335    #[serde(default)]
336    pub root: Option<String>,
337
338    /// Additional directories to include in the agent's scope.
339    #[serde(default)]
340    pub add_dirs: Vec<String>,
341
342    /// Per-step environment variables.
343    #[serde(default)]
344    pub env: HashMap<String, String>,
345
346    /// Files to attach to the agent prompt.
347    #[serde(default)]
348    pub files: Vec<String>,
349
350    /// Step-level reference files advertised in the system prompt.
351    ///
352    /// These are appended to the workflow-level `resources` for this specific
353    /// step. Paths are resolved relative to the `.zug` file's directory.
354    /// See [`ResourceSpec`] for the accepted shapes.
355    #[serde(default)]
356    pub resources: Vec<ResourceSpec>,
357
358    // --- Context injection ---
359    /// Session IDs to inject as context (beyond depends_on).
360    /// Maps to `--context <SESSION_ID>` flags on zag.
361    #[serde(default)]
362    pub context: Vec<String>,
363
364    /// Path to a plan file to prepend as context.
365    /// Maps to `--plan <PATH>` on zag.
366    #[serde(default)]
367    pub plan: Option<String>,
368
369    /// Per-step MCP configuration (JSON string or file path, Claude only).
370    /// Maps to `--mcp-config <CONFIG>` on zag.
371    #[serde(default)]
372    pub mcp_config: Option<String>,
373
374    // --- Isolation ---
375    /// If true, run this step in an isolated git worktree.
376    #[serde(default)]
377    pub worktree: bool,
378
379    /// Docker sandbox name. If set, the step runs inside a sandbox.
380    #[serde(default)]
381    pub sandbox: Option<String>,
382
383    // --- Advanced orchestration ---
384    /// Race group name. Steps sharing a race_group run in parallel;
385    /// when the first completes, the rest are cancelled.
386    #[serde(default)]
387    pub race_group: Option<String>,
388
389    /// Model to use when retrying this step (only applies when
390    /// on_failure = "retry"). Enables escalation to a larger model.
391    #[serde(default)]
392    pub retry_model: Option<String>,
393
394    // --- Command step types ---
395    /// Zag command to invoke for this step. Default (None) uses `zag run`.
396    /// Other options: "review", "plan", "pipe", "collect", "summary".
397    #[serde(default)]
398    pub command: Option<StepCommand>,
399
400    /// Review uncommitted changes (only valid when `command = "review"`).
401    #[serde(default)]
402    pub uncommitted: bool,
403
404    /// Base branch for review diff (only valid when `command = "review"`).
405    #[serde(default)]
406    pub base: Option<String>,
407
408    /// Specific commit to review (only valid when `command = "review"`).
409    #[serde(default)]
410    pub commit: Option<String>,
411
412    /// Title for the review (only valid when `command = "review"`).
413    #[serde(default)]
414    pub title: Option<String>,
415
416    /// Output path for generated plan (only valid when `command = "plan"`).
417    #[serde(default)]
418    pub plan_output: Option<String>,
419
420    /// Additional instructions for plan generation (only valid when `command = "plan"`).
421    #[serde(default)]
422    pub instructions: Option<String>,
423}
424
425/// What to do when a step fails.
426#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
427#[serde(rename_all = "lowercase")]
428pub enum FailurePolicy {
429    /// Abort the workflow (default).
430    #[default]
431    Fail,
432    /// Skip this step and continue.
433    Continue,
434    /// Retry the step up to `max_retries` times.
435    Retry,
436}
437
438/// Zag command type for a step. When set, changes which zag subcommand
439/// is invoked instead of the default `zag run`.
440#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
441#[serde(rename_all = "lowercase")]
442pub enum StepCommand {
443    /// Code review: `zag review`.
444    Review,
445    /// Implementation plan generation: `zag plan`.
446    Plan,
447    /// Chain session results into new agent: `zag pipe`.
448    Pipe,
449    /// Gather results from multiple sessions: `zag collect`.
450    Collect,
451    /// Log-based summary/stats: `zag summary`.
452    Summary,
453}
454
455impl std::fmt::Display for VarType {
456    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
457        match self {
458            VarType::String => write!(f, "string"),
459            VarType::Number => write!(f, "number"),
460            VarType::Bool => write!(f, "bool"),
461            VarType::Json => write!(f, "json"),
462        }
463    }
464}