Skip to content

Commit 2dbe162

Browse files
authored
feat: map engine max-turns and timeout-minutes to Copilot CLI arguments (#134)
1 parent b8e3049 commit 2dbe162

2 files changed

Lines changed: 121 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,49 @@ schedule:
285285
- release/*
286286
```
287287

288+
### Engine Configuration
289+
290+
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.
291+
292+
```yaml
293+
# Simple string format (just a model name)
294+
engine: claude-opus-4.5
295+
296+
# Object format with additional options
297+
engine:
298+
model: claude-opus-4.5
299+
max-turns: 50
300+
timeout-minutes: 30
301+
```
302+
303+
#### Fields
304+
305+
| Field | Type | Default | Description |
306+
|-------|------|---------|-------------|
307+
| `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. |
308+
| `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. |
309+
| `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. |
310+
311+
#### `max-turns`
312+
313+
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:
314+
315+
- **Cost control** — limiting expensive model invocations.
316+
- **Safety** — preventing infinite loops where the agent repeatedly calls tools without converging on a result.
317+
- **Predictability** — ensuring the pipeline completes within a reasonable time frame.
318+
319+
When omitted, the Copilot CLI uses its built-in default. When set, the compiler emits `--max-turns <value>` in the generated pipeline's copilot params.
320+
321+
#### `timeout-minutes`
322+
323+
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:
324+
325+
- **Budget enforcement** — hard-capping the total runtime of an agent to control compute costs.
326+
- **Pipeline hygiene** — preventing agents from occupying a runner indefinitely if they stall or enter long retry loops.
327+
- **SLA compliance** — ensuring scheduled agents complete within a known window.
328+
329+
When omitted, the Copilot CLI uses its built-in default. When set, the compiler emits `--max-timeout <value>` in the generated pipeline's copilot params.
330+
288331
### Tools Configuration
289332

290333
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
439482

440483
Additional params provided to agency CLI. The compiler generates:
441484
- `--model <model>` - AI model from `engine` front matter field (default: claude-opus-4.5)
485+
- `--max-turns <n>` - Maximum agentic turns from `engine.max-turns` (omitted when not set)
486+
- `--max-timeout <n>` - Workflow timeout in minutes from `engine.timeout-minutes` (omitted when not set)
442487
- `--disable-builtin-mcps` - Disables all built-in MCPs initially
443488
- `--no-ask-user` - Prevents interactive prompts
444489
- `--allow-tool <tool>` - Explicitly allows specific tools (github, safeoutputs, write, shell commands like cat, date, echo, grep, head, ls, pwd, sort, tail, uniq, wc, yq)

src/compile/common.rs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,28 @@ pub fn generate_copilot_params(front_matter: &FrontMatter) -> String {
310310
let mut params = Vec::new();
311311

312312
params.push(format!("--model {}", front_matter.engine.model()));
313+
if let Some(max_turns) = front_matter.engine.max_turns() {
314+
if max_turns == 0 {
315+
eprintln!(
316+
"Warning: Agent '{}' has max-turns: 0, which means zero turns allowed. \
317+
The agent will not be able to perform any tool calls. \
318+
Consider setting max-turns to at least 1.",
319+
front_matter.name
320+
);
321+
}
322+
params.push(format!("--max-turns {}", max_turns));
323+
}
324+
if let Some(timeout_minutes) = front_matter.engine.timeout_minutes() {
325+
if timeout_minutes == 0 {
326+
eprintln!(
327+
"Warning: Agent '{}' has timeout-minutes: 0, which means no time is allowed. \
328+
The agent session will time out immediately. \
329+
Consider setting timeout-minutes to at least 1.",
330+
front_matter.name
331+
);
332+
}
333+
params.push(format!("--max-timeout {}", timeout_minutes));
334+
}
313335
params.push("--disable-builtin-mcps".to_string());
314336
params.push("--no-ask-user".to_string());
315337

@@ -894,6 +916,60 @@ mod tests {
894916
assert!(params.contains("--mcp ado"));
895917
}
896918

919+
#[test]
920+
fn test_copilot_params_max_turns() {
921+
let (fm, _) = parse_markdown(
922+
"---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n max-turns: 50\n---\n",
923+
)
924+
.unwrap();
925+
let params = generate_copilot_params(&fm);
926+
assert!(params.contains("--max-turns 50"));
927+
}
928+
929+
#[test]
930+
fn test_copilot_params_no_max_turns_when_simple_engine() {
931+
let fm = minimal_front_matter();
932+
let params = generate_copilot_params(&fm);
933+
assert!(!params.contains("--max-turns"));
934+
}
935+
936+
#[test]
937+
fn test_copilot_params_max_timeout() {
938+
let (fm, _) = parse_markdown(
939+
"---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n timeout-minutes: 30\n---\n",
940+
)
941+
.unwrap();
942+
let params = generate_copilot_params(&fm);
943+
assert!(params.contains("--max-timeout 30"));
944+
}
945+
946+
#[test]
947+
fn test_copilot_params_no_max_timeout_when_simple_engine() {
948+
let fm = minimal_front_matter();
949+
let params = generate_copilot_params(&fm);
950+
assert!(!params.contains("--max-timeout"));
951+
}
952+
953+
#[test]
954+
fn test_copilot_params_max_turns_zero_still_emitted() {
955+
let (fm, _) = parse_markdown(
956+
"---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n max-turns: 0\n---\n",
957+
)
958+
.unwrap();
959+
let params = generate_copilot_params(&fm);
960+
assert!(params.contains("--max-turns 0"));
961+
}
962+
963+
#[test]
964+
fn test_copilot_params_max_timeout_zero_still_emitted() {
965+
let (fm, _) = parse_markdown(
966+
"---\nname: test\ndescription: test\nengine:\n model: claude-opus-4.5\n timeout-minutes: 0\n---\n",
967+
)
968+
.unwrap();
969+
let params = generate_copilot_params(&fm);
970+
assert!(params.contains("--max-timeout 0"));
971+
}
972+
897973
// ─── sanitize_filename ────────────────────────────────────────────────────
898974

899975
#[test]

0 commit comments

Comments
 (0)