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}