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}