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}