Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/patch-max-continuations.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
"ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
"ghcr.io/devcontainers/features/copilot-cli:latest": {},
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/git-lfs:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
"ghcr.io/devcontainers/features/node:1": {
"version": "24"
}
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/smoke-copilot.lock.yml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion .github/workflows/smoke-copilot.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ permissions:
discussions: read
actions: read
name: Smoke Copilot
engine: copilot
engine:
id: copilot
max-continuations: 2
imports:
- shared/gh.md
- shared/reporting.md
Expand Down
9 changes: 9 additions & 0 deletions pkg/parser/schemas/main_workflow_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -7413,6 +7413,10 @@
"id": "copilot",
"version": "beta"
},
{
"id": "copilot",
"max-continuations": 5
},
{
"id": "claude",
"concurrency": {
Expand Down Expand Up @@ -7458,6 +7462,11 @@
],
"description": "Maximum number of chat iterations per run. Helps prevent runaway loops and control costs. Has sensible defaults and can typically be omitted. Note: Only supported by the claude engine."
},
"max-continuations": {
"type": "integer",
"minimum": 1,
"description": "Maximum number of continuations for multi-run autopilot mode. Default is 1 (single run, no autopilot). Values greater than 1 enable --autopilot mode for the copilot engine with --max-autopilot-continues set to this value. Note: Only supported by the copilot engine."
},
"concurrency": {
"oneOf": [
{
Expand Down
19 changes: 19 additions & 0 deletions pkg/workflow/agent_validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
//
// - validateAgentFile() - Validates custom agent file exists
// - validateMaxTurnsSupport() - Validates max-turns feature support
// - validateMaxContinuationsSupport() - Validates max-continuations feature support
// - validateWebSearchSupport() - Validates web-search feature support (warning)
// - validateWorkflowRunBranches() - Validates workflow_run has branch restrictions
//
Expand Down Expand Up @@ -122,6 +123,24 @@ func (c *Compiler) validateMaxTurnsSupport(frontmatter map[string]any, engine Co
return nil
}

// validateMaxContinuationsSupport validates that max-continuations is only used with engines that support this feature
func (c *Compiler) validateMaxContinuationsSupport(frontmatter map[string]any, engine CodingAgentEngine) error {
// Check if max-continuations is specified in the engine config
_, engineConfig := c.ExtractEngineConfig(frontmatter)

if engineConfig == nil || engineConfig.MaxContinuations == 0 {
// No max-continuations specified, no validation needed
return nil
}
Comment on lines +126 to +134
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are existing integration tests for max-turns engine support validation (pkg/workflow/max_turns_validation_test.go), but no corresponding tests for the new max-continuations validation. Add coverage that asserts: (1) max-continuations errors on engines that don’t support it (e.g., claude/codex), and (2) it succeeds for copilot.

Copilot uses AI. Check for mistakes.

// max-continuations is specified, check if the engine supports it
if !engine.SupportsMaxContinuations() {
return fmt.Errorf("max-continuations not supported: engine '%s' does not support the max-continuations feature", engine.GetID())
}

return nil
}

// validateWebSearchSupport validates that web-search tool is only used with engines that support this feature
func (c *Compiler) validateWebSearchSupport(tools map[string]any, engine CodingAgentEngine) {
// Check if web-search tool is requested
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The validation pattern here mirrors validateMaxTurnsSupport nicely — good consistency. Worth noting the guard engineConfig.MaxContinuations == 0 correctly treats 0 as "not set" since minimum valid value is 1 per schema.

Expand Down
31 changes: 20 additions & 11 deletions pkg/workflow/agentic_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ type CapabilityProvider interface {
// When true, plugins can be installed using the engine's plugin install command
SupportsPlugins() bool

// SupportsMaxContinuations returns true if this engine supports the max-continuations feature
// When true, max-continuations > 1 enables autopilot/multi-run mode for the engine
SupportsMaxContinuations() bool

// SupportsLLMGateway returns the LLM gateway port number for this engine
// Returns the port number (e.g., 10000) if the engine supports an LLM gateway
// Returns -1 if the engine does not support an LLM gateway
Expand Down Expand Up @@ -207,17 +211,18 @@ type CodingAgentEngine interface {

// BaseEngine provides common functionality for agentic engines
type BaseEngine struct {
id string
displayName string
description string
experimental bool
supportsToolsAllowlist bool
supportsMaxTurns bool
supportsWebFetch bool
supportsWebSearch bool
supportsFirewall bool
supportsPlugins bool
supportsLLMGateway bool
id string
displayName string
description string
experimental bool
supportsToolsAllowlist bool
supportsMaxTurns bool
supportsMaxContinuations bool
supportsWebFetch bool
supportsWebSearch bool
supportsFirewall bool
supportsPlugins bool
supportsLLMGateway bool
}

func (e *BaseEngine) GetID() string {
Expand Down Expand Up @@ -260,6 +265,10 @@ func (e *BaseEngine) SupportsPlugins() bool {
return e.supportsPlugins
}

func (e *BaseEngine) SupportsMaxContinuations() bool {
return e.supportsMaxContinuations
}

func (e *BaseEngine) SupportsLLMGateway() int {
// Engines that support LLM gateway must override this method
// to return their specific port number (e.g., 10000, 10001, 10002)
Expand Down
5 changes: 5 additions & 0 deletions pkg/workflow/compiler_orchestrator_tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,11 @@ func (c *Compiler) processToolsAndMarkdown(result *parser.FrontmatterResult, cle
return nil, err
}

// Validate max-continuations support for the current engine
if err := c.validateMaxContinuationsSupport(result.Frontmatter, agenticEngine); err != nil {
return nil, err
}

// Validate web-search support for the current engine (warning only)
c.validateWebSearchSupport(tools, agenticEngine)

Expand Down
23 changes: 12 additions & 11 deletions pkg/workflow/copilot_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,18 @@ func NewCopilotEngine() *CopilotEngine {
copilotLog.Print("Creating new Copilot engine instance")
return &CopilotEngine{
BaseEngine: BaseEngine{
id: "copilot",
displayName: "GitHub Copilot CLI",
description: "Uses GitHub Copilot CLI with MCP server support",
experimental: false,
supportsToolsAllowlist: true,
supportsMaxTurns: false, // Copilot CLI does not support max-turns feature yet
supportsWebFetch: true, // Copilot CLI has built-in web-fetch support
supportsWebSearch: false, // Copilot CLI does not have built-in web-search support
supportsFirewall: true, // Copilot supports network firewalling via AWF
supportsPlugins: true, // Copilot supports plugin installation
supportsLLMGateway: true, // Copilot supports LLM gateway on port 10003
id: "copilot",
displayName: "GitHub Copilot CLI",
description: "Uses GitHub Copilot CLI with MCP server support",
experimental: false,
supportsToolsAllowlist: true,
supportsMaxTurns: false, // Copilot CLI does not support max-turns feature yet
supportsMaxContinuations: true, // Copilot CLI supports --autopilot with --max-autopilot-continues
supportsWebFetch: true, // Copilot CLI has built-in web-fetch support
supportsWebSearch: false, // Copilot CLI does not have built-in web-search support
supportsFirewall: true, // Copilot supports network firewalling via AWF
supportsPlugins: true, // Copilot supports plugin installation
supportsLLMGateway: true, // Copilot supports LLM gateway on port 10003
},
}
}
Expand Down
11 changes: 9 additions & 2 deletions pkg/workflow/copilot_engine_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st
copilotArgs = append(copilotArgs, "--agent", agentIdentifier)
}

// Add --autopilot and --max-autopilot-continues when max-continuations > 1
// Never apply autopilot flags to detection jobs; they are only meaningful for the agent run.
isDetectionJob := workflowData.SafeOutputs == nil
if !isDetectionJob && workflowData.EngineConfig != nil && workflowData.EngineConfig.MaxContinuations > 1 {
maxCont := workflowData.EngineConfig.MaxContinuations
Comment on lines +89 to +93
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add unit/integration coverage for the new autopilot argument behavior: when EngineConfig.MaxContinuations > 1 and this is an agent run (SafeOutputs != nil), the generated Copilot CLI command should include --autopilot --max-autopilot-continues <n>, and those flags should be absent for detection jobs (SafeOutputs == nil). There are already similar assertions in pkg/workflow/copilot_engine_test.go for other flags.

Copilot uses AI. Check for mistakes.
copilotExecLog.Printf("Enabling autopilot mode with max-autopilot-continues=%d", maxCont)
copilotArgs = append(copilotArgs, "--autopilot", "--max-autopilot-continues", strconv.Itoa(maxCont))
}

// Add tool permission arguments based on configuration
toolArgs := e.computeCopilotToolArguments(workflowData.Tools, workflowData.SafeOutputs, workflowData.SafeInputs, workflowData)
if len(toolArgs) > 0 {
Expand Down Expand Up @@ -144,7 +153,6 @@ func (e *CopilotEngine) GetExecutionSteps(workflowData *WorkflowData, logFile st
// Copilot CLI reads it natively - no --model flag in the shell command needed.
needsModelFlag := !modelConfigured
// Check if this is a detection job (has no SafeOutputs config)
isDetectionJob := workflowData.SafeOutputs == nil
var modelEnvVar string
if isDetectionJob {
modelEnvVar = constants.EnvVarModelDetectionCopilot
Expand Down Expand Up @@ -303,7 +311,6 @@ COPILOT_CLI_INSTRUCTION="$(cat /tmp/gh-aw/aw-prompts/prompt.txt)"
env[constants.CopilotCLIModelEnvVar] = workflowData.EngineConfig.Model
} else {
// No model configured - use fallback GitHub variable with shell expansion
isDetectionJob := workflowData.SafeOutputs == nil
if isDetectionJob {
env[constants.EnvVarModelDetectionCopilot] = fmt.Sprintf("${{ vars.%s || '' }}", constants.EnvVarModelDetectionCopilot)
} else {
Expand Down
38 changes: 26 additions & 12 deletions pkg/workflow/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,19 @@ var engineLog = logger.New("workflow:engine")

// EngineConfig represents the parsed engine configuration
type EngineConfig struct {
ID string
Version string
Model string
MaxTurns string
Concurrency string // Agent job-level concurrency configuration (YAML format)
UserAgent string
Command string // Custom executable path (when set, skip installation steps)
Env map[string]string
Config string
Args []string
Firewall *FirewallConfig // AWF firewall configuration
Agent string // Agent identifier for copilot --agent flag (copilot engine only)
ID string
Version string
Model string
MaxTurns string
MaxContinuations int // Maximum number of continuations for autopilot mode (copilot engine only; > 1 enables --autopilot)
Concurrency string // Agent job-level concurrency configuration (YAML format)
UserAgent string
Command string // Custom executable path (when set, skip installation steps)
Env map[string]string
Config string
Args []string
Firewall *FirewallConfig // AWF firewall configuration
Agent string // Agent identifier for copilot --agent flag (copilot engine only)
}

// NetworkPermissions represents network access permissions for workflow execution
Expand Down Expand Up @@ -116,6 +117,19 @@ func (c *Compiler) ExtractEngineConfig(frontmatter map[string]any) (string, *Eng
}
}

// Extract optional 'max-continuations' field
if maxCont, hasMaxCont := engineObj["max-continuations"]; hasMaxCont {
if maxContInt, ok := maxCont.(int); ok {
config.MaxContinuations = maxContInt
} else if maxContUint64, ok := maxCont.(uint64); ok {
config.MaxContinuations = int(maxContUint64)
Comment on lines +121 to +125
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

max-continuations parsing only handles int, uint64, and numeric strings. When engine config comes from JSON (e.g., included engine config via extractEngineConfigFromJSON), numbers are decoded as float64, so max-continuations will be silently ignored and autopilot won’t be enabled. Consider using the existing int-conversion helpers (e.g., ConvertToInt / parseIntValue / ParseIntFromConfig) and/or handling float64/int64/json.Number explicitly; also emit a warning/error when the key is present but unparseable.

This issue also appears on line 124 of the same file.

See below for a potential fix:

				switch v := maxTurns.(type) {
				case int:
					config.MaxTurns = strconv.Itoa(v)
				case int64:
					config.MaxTurns = strconv.FormatInt(v, 10)
				case uint64:
					config.MaxTurns = strconv.FormatUint(v, 10)
				case float64:
					// JSON numbers are decoded as float64; coerce to int64 for formatting.
					config.MaxTurns = strconv.FormatInt(int64(v), 10)
				case json.Number:
					// Preserve the textual representation; assumed to be numeric.
					config.MaxTurns = v.String()
				case string:
					config.MaxTurns = v
				default:
					engineLog.Printf("Unsupported type for 'max-turns': %T (value: %v); ignoring", v, v)
				}
			}

			// Extract optional 'max-continuations' field
			if maxCont, hasMaxCont := engineObj["max-continuations"]; hasMaxCont {
				switch v := maxCont.(type) {
				case int:
					config.MaxContinuations = v
				case int64:
					config.MaxContinuations = int(v)
				case uint64:
					config.MaxContinuations = int(v)
				case float64:
					// JSON numbers are decoded as float64; coerce to int.
					config.MaxContinuations = int(v)
				case json.Number:
					if parsed, err := strconv.Atoi(v.String()); err == nil {
						config.MaxContinuations = parsed
					} else {
						engineLog.Printf("Invalid numeric value for 'max-continuations': %v; ignoring", v)
					}
				case string:
					if parsed, err := strconv.Atoi(v); err == nil {
						config.MaxContinuations = parsed
					} else {
						engineLog.Printf("Invalid string value for 'max-continuations': %q; ignoring", v)
					}
				default:
					engineLog.Printf("Unsupported type for 'max-continuations': %T (value: %v); ignoring", v, v)

Copilot uses AI. Check for mistakes.
} else if maxContStr, ok := maxCont.(string); ok {
if parsed, err := strconv.Atoi(maxContStr); err == nil {
config.MaxContinuations = parsed
}
}
}

// Extract optional 'concurrency' field (string or object format)
if concurrency, hasConcurrency := engineObj["concurrency"]; hasConcurrency {
if concurrencyStr, ok := concurrency.(string); ok {
Expand Down
1 change: 1 addition & 0 deletions smoke-test-22406871179.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Test file for PR push - Run 22406871179
Loading