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