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