diff --git a/AGENTS.md b/AGENTS.md index 6d7cecd..d5fc0b1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -285,6 +285,49 @@ schedule: - release/* ``` +### Engine Configuration + +The `engine` field specifies which AI model to use and optional execution parameters. It accepts both a simple string format (model name only) and an object format with additional options. + +```yaml +# Simple string format (just a model name) +engine: claude-opus-4.5 + +# Object format with additional options +engine: + model: claude-opus-4.5 + max-turns: 50 + timeout-minutes: 30 +``` + +#### Fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `model` | string | `claude-opus-4.5` | AI model to use. Options include `claude-sonnet-4.5`, `gpt-5.2-codex`, `gemini-3-pro-preview`, etc. | +| `max-turns` | integer | *(none)* | Maximum number of agentic turns (tool-use iterations) the model is allowed per run. Maps to the `--max-turns` Copilot CLI argument. Use this to cap compute and prevent runaway loops. | +| `timeout-minutes` | integer | *(none)* | Maximum time in minutes the agent workflow is allowed to run. Maps to the `--max-timeout` Copilot CLI argument. Use this to cap long-running agent sessions. | + +#### `max-turns` + +Each "turn" is one iteration of the model calling a tool and receiving its output. Setting `max-turns` places an upper bound on how many such iterations the agent can perform in a single pipeline run. This is useful for: + +- **Cost control** — limiting expensive model invocations. +- **Safety** — preventing infinite loops where the agent repeatedly calls tools without converging on a result. +- **Predictability** — ensuring the pipeline completes within a reasonable time frame. + +When omitted, the Copilot CLI uses its built-in default. When set, the compiler emits `--max-turns ` in the generated pipeline's copilot params. + +#### `timeout-minutes` + +The `timeout-minutes` field sets a wall-clock limit (in minutes) for the entire agent session. It maps to the `--max-timeout` Copilot CLI argument. This is useful for: + +- **Budget enforcement** — hard-capping the total runtime of an agent to control compute costs. +- **Pipeline hygiene** — preventing agents from occupying a runner indefinitely if they stall or enter long retry loops. +- **SLA compliance** — ensuring scheduled agents complete within a known window. + +When omitted, the Copilot CLI uses its built-in default. When set, the compiler emits `--max-timeout ` in the generated pipeline's copilot params. + ### Tools Configuration The `tools` field controls which tools are available to the agent. Both sub-fields are optional and have sensible defaults. @@ -439,6 +482,8 @@ Should be replaced with the human-readable name from the front matter (e.g., "Da Additional params provided to agency CLI. The compiler generates: - `--model ` - AI model from `engine` front matter field (default: claude-opus-4.5) +- `--max-turns ` - Maximum agentic turns from `engine.max-turns` (omitted when not set) +- `--max-timeout ` - Workflow timeout in minutes from `engine.timeout-minutes` (omitted when not set) - `--disable-builtin-mcps` - Disables all built-in MCPs initially - `--no-ask-user` - Prevents interactive prompts - `--allow-tool ` - Explicitly allows specific tools (github, safeoutputs, write, shell commands like cat, date, echo, grep, head, ls, pwd, sort, tail, uniq, wc, yq) diff --git a/src/compile/common.rs b/src/compile/common.rs index 26c1d33..60522f0 100644 --- a/src/compile/common.rs +++ b/src/compile/common.rs @@ -310,6 +310,28 @@ pub fn generate_copilot_params(front_matter: &FrontMatter) -> String { let mut params = Vec::new(); params.push(format!("--model {}", front_matter.engine.model())); + if let Some(max_turns) = front_matter.engine.max_turns() { + if max_turns == 0 { + eprintln!( + "Warning: Agent '{}' has max-turns: 0, which means zero turns allowed. \ + The agent will not be able to perform any tool calls. \ + Consider setting max-turns to at least 1.", + front_matter.name + ); + } + params.push(format!("--max-turns {}", max_turns)); + } + if let Some(timeout_minutes) = front_matter.engine.timeout_minutes() { + if timeout_minutes == 0 { + eprintln!( + "Warning: Agent '{}' has timeout-minutes: 0, which means no time is allowed. \ + The agent session will time out immediately. \ + Consider setting timeout-minutes to at least 1.", + front_matter.name + ); + } + params.push(format!("--max-timeout {}", timeout_minutes)); + } params.push("--disable-builtin-mcps".to_string()); params.push("--no-ask-user".to_string()); @@ -894,6 +916,60 @@ mod tests { assert!(params.contains("--mcp ado")); } + #[test] + fn test_copilot_params_max_turns() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n max-turns: 50\n---\n", + ) + .unwrap(); + let params = generate_copilot_params(&fm); + assert!(params.contains("--max-turns 50")); + } + + #[test] + fn test_copilot_params_no_max_turns_when_simple_engine() { + let fm = minimal_front_matter(); + let params = generate_copilot_params(&fm); + assert!(!params.contains("--max-turns")); + } + + #[test] + fn test_copilot_params_max_timeout() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n timeout-minutes: 30\n---\n", + ) + .unwrap(); + let params = generate_copilot_params(&fm); + assert!(params.contains("--max-timeout 30")); + } + + #[test] + fn test_copilot_params_no_max_timeout_when_simple_engine() { + let fm = minimal_front_matter(); + let params = generate_copilot_params(&fm); + assert!(!params.contains("--max-timeout")); + } + + #[test] + fn test_copilot_params_max_turns_zero_still_emitted() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n max-turns: 0\n---\n", + ) + .unwrap(); + let params = generate_copilot_params(&fm); + assert!(params.contains("--max-turns 0")); + } + + #[test] + fn test_copilot_params_max_timeout_zero_still_emitted() { + let (fm, _) = parse_markdown( + "---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n timeout-minutes: 0\n---\n", + ) + .unwrap(); + let params = generate_copilot_params(&fm); + assert!(params.contains("--max-timeout 0")); + } + // ─── sanitize_filename ──────────────────────────────────────────────────── #[test]