Skip to content
Closed
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
11 changes: 9 additions & 2 deletions pkg/workflow/awf_command_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ func BuildAWFCommand(config AWFCommandConfig) string {
if err != nil {
awfHelpersLog.Printf("Warning: failed to build AWF config JSON: %v", err)
} else {
if config.WorkflowData != nil && config.WorkflowData.EngineConfig != nil && config.WorkflowData.EngineConfig.MaxTurnCacheMissesExpression != "" {
awfConfigJSON = injectMaxTurnCacheMissesExpression(awfConfigJSON, fmt.Sprintf("${%s}", awfMaxTurnCacheMissesVarName))
}
configFileSetup = buildAWFConfigFileSetup(config, awfConfigJSON)
expandableArgs = fmt.Sprintf("--config %q ", awfConfigRuntimePathExpr) + expandableArgs
awfHelpersLog.Print("Using AWF config file (--config flag)")
Expand Down Expand Up @@ -183,7 +186,8 @@ func appendArcDindMountSettings(expandableArgs, arcDindDockerHostProbe string, i

func buildAWFConfigFileSetup(config AWFCommandConfig, awfConfigJSON string) string {
maxAICreditsExportLine, updatedAWFConfigJSON := buildMaxAICreditsExport(config, awfConfigJSON)
printfArg := buildAWFConfigPrintfArg(updatedAWFConfigJSON, maxAICreditsExportLine != "")
preserveMaxTurnCacheMisses := config.WorkflowData != nil && config.WorkflowData.EngineConfig != nil && config.WorkflowData.EngineConfig.MaxTurnCacheMissesExpression != ""
printfArg := buildAWFConfigPrintfArg(updatedAWFConfigJSON, maxAICreditsExportLine != "", preserveMaxTurnCacheMisses)
configFileSetup := buildConfigFilePrintfLine(printfArg)
if maxAICreditsExportLine != "" {
configFileSetup = maxAICreditsExportLine + "\n" + configFileSetup
Expand Down Expand Up @@ -230,11 +234,14 @@ func buildMaxAICreditsExport(config AWFCommandConfig, awfConfigJSON string) (str
return fmt.Sprintf(`%s="%s"`, awfMaxAICreditsVarName, expr), awfConfigJSON
}

func buildAWFConfigPrintfArg(awfConfigJSON string, hasMaxAICreditsExport bool) string {
func buildAWFConfigPrintfArg(awfConfigJSON string, hasMaxAICreditsExport, preserveMaxTurnCacheMisses bool) string {
preservedVars := make([]string, 0, 2)
if hasMaxAICreditsExport {
preservedVars = append(preservedVars, awfMaxAICreditsVarName)
}
if preserveMaxTurnCacheMisses {
preservedVars = append(preservedVars, awfMaxTurnCacheMissesVarName)
}
if strings.Contains(awfConfigJSON, awfArcDindRootPathExpr) {
preservedVars = append(preservedVars, "RUNNER_TEMP")
}
Expand Down
20 changes: 20 additions & 0 deletions pkg/workflow/awf_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,26 @@ func injectMaxAICreditsExpression(awfConfigJSON string, expr string) string {
return awfConfigJSON[:valueEnd] + `,"maxAiCredits":` + expr + awfConfigJSON[valueEnd:]
}

// injectMaxTurnCacheMissesExpression replaces the apiProxy maxCacheMisses
// integer with a GitHub Actions expression.
func injectMaxTurnCacheMissesExpression(awfConfigJSON string, expr string) string {
const maxTurnCacheMissesKey = `"maxCacheMisses":`
idx := strings.Index(awfConfigJSON, maxTurnCacheMissesKey)
if idx == -1 {
awfHelpersLog.Print("Warning: could not find maxCacheMisses in AWF config JSON; expression not injected")
return awfConfigJSON
}
valueEnd := idx + len(maxTurnCacheMissesKey)
for valueEnd < len(awfConfigJSON) && awfConfigJSON[valueEnd] >= '0' && awfConfigJSON[valueEnd] <= '9' {
valueEnd++
}
if valueEnd == idx+len(maxTurnCacheMissesKey) {
awfHelpersLog.Print("Warning: maxCacheMisses is not an integer in AWF config JSON; expression not injected")
return awfConfigJSON
}
return awfConfigJSON[:idx+len(maxTurnCacheMissesKey)] + expr + awfConfigJSON[valueEnd:]
}

// ComputeAWFExcludeEnvVarNames returns the list of environment variable names that must be
// excluded from the agent container's visible environment via AWF's --exclude-env flag.
//
Expand Down
13 changes: 13 additions & 0 deletions pkg/workflow/awf_env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ func TestInjectMaxAICreditsExpressionWithoutMaxRunsLeavesJSONUnchanged(t *testin
}
}

func TestInjectMaxTurnCacheMissesExpression(t *testing.T) {
configJSON := `{"apiProxy":{"maxCacheMisses":5}}`

got := injectMaxTurnCacheMissesExpression(configJSON, "${GH_AW_MAX_TURN_CACHE_MISSES}")
assert.Contains(t, got, `"maxCacheMisses":${GH_AW_MAX_TURN_CACHE_MISSES}`)
}

func TestInjectMaxTurnCacheMissesExpressionWithInvalidValueLeavesJSONUnchanged(t *testing.T) {
configJSON := `{"apiProxy":{"maxCacheMisses":null}}`

assert.JSONEq(t, configJSON, injectMaxTurnCacheMissesExpression(configJSON, "${GH_AW_MAX_TURN_CACHE_MISSES}"))
}

func TestApplyDefaultMaxAICreditsEnvToMapHandlesNilMap(t *testing.T) {
assert.NotPanics(t, func() {
applyDefaultMaxAICreditsEnvToMap(nil, nil)
Expand Down
19 changes: 10 additions & 9 deletions pkg/workflow/awf_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ import (
var awfHelpersLog = logger.New("workflow:awf_helpers")

const (
awfDockerHostVarName = "GH_AW_DOCKER_HOST"
awfToolCacheMountVarName = "GH_AW_TOOL_CACHE_MOUNT"
awfMaxAICreditsVarName = "GH_AW_MAX_AI_CREDITS"
awfConfigRuntimePathExpr = "${RUNNER_TEMP}/gh-aw/awf-config.json"
awfModelsJSONPathExpr = "/tmp/gh-aw/models.json"
awfArcDindRootPathExpr = "${RUNNER_TEMP}/gh-aw"
awfArcDindHomePathExpr = "${RUNNER_TEMP}/gh-aw/home"
awfArcDindProxyLogsDirExpr = "${RUNNER_TEMP}/gh-aw/sandbox/firewall/logs"
awfArcDindAuditDirExpr = "${RUNNER_TEMP}/gh-aw/sandbox/firewall/audit"
awfDockerHostVarName = "GH_AW_DOCKER_HOST"
awfToolCacheMountVarName = "GH_AW_TOOL_CACHE_MOUNT"
awfMaxAICreditsVarName = "GH_AW_MAX_AI_CREDITS"
awfMaxTurnCacheMissesVarName = "GH_AW_MAX_TURN_CACHE_MISSES"
awfConfigRuntimePathExpr = "${RUNNER_TEMP}/gh-aw/awf-config.json"
awfModelsJSONPathExpr = "/tmp/gh-aw/models.json"
awfArcDindRootPathExpr = "${RUNNER_TEMP}/gh-aw"
awfArcDindHomePathExpr = "${RUNNER_TEMP}/gh-aw/home"
awfArcDindProxyLogsDirExpr = "${RUNNER_TEMP}/gh-aw/sandbox/firewall/logs"
awfArcDindAuditDirExpr = "${RUNNER_TEMP}/gh-aw/sandbox/firewall/audit"
// Bash regex used in [[ ... =~ ... ]] to detect TCP Docker hosts (ARC/DinD).
// Any tcp:// DOCKER_HOST indicates the Docker daemon runs on a separate filesystem,
// requiring --docker-host so AWF connects to the correct daemon.
Expand Down
54 changes: 31 additions & 23 deletions pkg/workflow/compiler_orchestrator_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type engineSetupResult struct {
func (c *Compiler) setupEngineAndImports(result *parser.FrontmatterResult, cleanPath string, content []byte, markdownDir string) (*engineSetupResult, error) {
orchestratorEngineLog.Printf("Setting up engine and processing imports")
engineSetting, engineConfig, model := c.ExtractEngineConfig(result.Frontmatter)
preservedMaxTurns, preservedMaxAICredits, preservedMaxRuns, preservedMaxTurnCacheMisses := extractEngineBudgetLimits(engineConfig)
preservedMaxTurns, preservedMaxAICredits, preservedMaxRuns, preservedMaxTurnCacheMisses, preservedMaxTurnCacheMissesExpression := extractEngineBudgetLimits(engineConfig)
if err := c.validateAndRegisterInlineEngineConfig(engineConfig); err != nil {
return nil, err
}
Expand Down Expand Up @@ -73,15 +73,16 @@ func (c *Compiler) setupEngineAndImports(result *parser.FrontmatterResult, clean
return nil, err
}
engineConfig, model = c.applyEngineImportDefaults(engineImportDefaultsOptions{
engineConfig: engineConfig,
model: model,
engineSetting: engineSetting,
importsResult: importsResult,
importedEngineDefinitions: importedEngineDefinitions,
preservedMaxTurns: preservedMaxTurns,
preservedMaxAICredits: preservedMaxAICredits,
preservedMaxRuns: preservedMaxRuns,
preservedMaxTurnCacheMisses: preservedMaxTurnCacheMisses,
engineConfig: engineConfig,
model: model,
engineSetting: engineSetting,
importsResult: importsResult,
importedEngineDefinitions: importedEngineDefinitions,
preservedMaxTurns: preservedMaxTurns,
preservedMaxAICredits: preservedMaxAICredits,
preservedMaxRuns: preservedMaxRuns,
preservedMaxTurnCacheMisses: preservedMaxTurnCacheMisses,
preservedMaxTurnCacheMissesExpression: preservedMaxTurnCacheMissesExpression,
})
agenticEngine, configSteps, err := c.resolveEngineRuntimeConfig(engineSetting, engineConfig)
if err != nil {
Expand Down Expand Up @@ -122,11 +123,11 @@ func frontmatterDeclaresImports(frontmatter map[string]any) bool {
return false
}

func extractEngineBudgetLimits(engineConfig *EngineConfig) (string, int64, int, int) {
func extractEngineBudgetLimits(engineConfig *EngineConfig) (string, int64, int, int, string) {
if engineConfig == nil {
return "", 0, 0, 0
return "", 0, 0, 0, ""
}
return engineConfig.MaxTurns, engineConfig.MaxAICredits, engineConfig.MaxRuns, engineConfig.MaxTurnCacheMisses
return engineConfig.MaxTurns, engineConfig.MaxAICredits, engineConfig.MaxRuns, engineConfig.MaxTurnCacheMisses, engineConfig.MaxTurnCacheMissesExpression
}

func defaultNetworkPermissions(networkPermissions *NetworkPermissions) *NetworkPermissions {
Expand Down Expand Up @@ -355,15 +356,16 @@ func (c *Compiler) resolveEngineFromIncludesAndImports(
}

type engineImportDefaultsOptions struct {
engineConfig *EngineConfig
model string
engineSetting string
importsResult *parser.ImportsResult
importedEngineDefinitions []string
preservedMaxTurns string
preservedMaxAICredits int64
preservedMaxRuns int
preservedMaxTurnCacheMisses int
engineConfig *EngineConfig
model string
engineSetting string
importsResult *parser.ImportsResult
importedEngineDefinitions []string
preservedMaxTurns string
preservedMaxAICredits int64
preservedMaxRuns int
preservedMaxTurnCacheMisses int
preservedMaxTurnCacheMissesExpression string
}

// applyEngineImportDefaults merges import-derived engine defaults into engineConfig.
Expand All @@ -388,6 +390,9 @@ func (c *Compiler) applyEngineImportDefaults(opts engineImportDefaultsOptions) (
if opts.preservedMaxTurnCacheMisses > 0 {
engineConfig.MaxTurnCacheMisses = opts.preservedMaxTurnCacheMisses
}
if opts.preservedMaxTurnCacheMissesExpression != "" {
engineConfig.MaxTurnCacheMissesExpression = opts.preservedMaxTurnCacheMissesExpression
}
if engineConfig.MaxTurns == "" && opts.importsResult.MergedMaxTurns != "" {
var importedMaxTurns any
if err := json.Unmarshal([]byte(opts.importsResult.MergedMaxTurns), &importedMaxTurns); err == nil {
Expand Down Expand Up @@ -424,12 +429,15 @@ func (c *Compiler) applyEngineImportDefaults(opts engineImportDefaultsOptions) (
}
}
}
if engineConfig.MaxTurnCacheMisses <= 0 && opts.importsResult.MergedMaxTurnCacheMisses != "" {
if engineConfig.MaxTurnCacheMisses <= 0 && engineConfig.MaxTurnCacheMissesExpression == "" && opts.importsResult.MergedMaxTurnCacheMisses != "" {
var importedMaxTurnCacheMisses any
if err := json.Unmarshal([]byte(opts.importsResult.MergedMaxTurnCacheMisses), &importedMaxTurnCacheMisses); err == nil {
if parsed := parseMaxTurnCacheMissesValue(importedMaxTurnCacheMisses); parsed > 0 {
engineConfig.MaxTurnCacheMisses = parsed
orchestratorEngineLog.Printf("Applied max-turn-cache-misses from import")
} else if expression := parseMaxTurnCacheMissesExpression(importedMaxTurnCacheMisses); expression != "" {
engineConfig.MaxTurnCacheMissesExpression = expression
orchestratorEngineLog.Printf("Applied max-turn-cache-misses expression from import")
}
}
}
Expand Down
31 changes: 31 additions & 0 deletions pkg/workflow/compiler_orchestrator_engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"testing"

"github.com/github/gh-aw/pkg/parser"
"github.com/github/gh-aw/pkg/stringutil"
"github.com/github/gh-aw/pkg/testutil"
"github.com/github/gh-aw/pkg/workflow/compilerenv"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -299,6 +300,36 @@ imports:
assert.Equal(t, 7, result.engineConfig.MaxTurnCacheMisses)
}

func TestCompileWorkflowAllowsImportedMaxTurnCacheMissesExpression(t *testing.T) {
tmpDir := testutil.TempDir(t, "engine-imported-max-turn-cache-misses-expression")

sharedDir := filepath.Join(tmpDir, "shared")
require.NoError(t, os.MkdirAll(sharedDir, 0755))
require.NoError(t, os.WriteFile(filepath.Join(sharedDir, "common.md"), []byte(`---
max-turn-cache-misses: "${{ inputs.max_turn_cache_misses }}"
---

# Shared Workflow
`), 0644))

testFile := filepath.Join(tmpDir, "test.md")
require.NoError(t, os.WriteFile(testFile, []byte(`---
on: push
engine: copilot
imports:
- shared/common.md
---

# Test Workflow
`), 0644))

require.NoError(t, NewCompiler().CompileWorkflow(testFile))
lockContent, err := os.ReadFile(stringutil.MarkdownToLockFile(testFile))
require.NoError(t, err)
assert.Contains(t, string(lockContent), "GH_AW_MAX_TURN_CACHE_MISSES: ${{ inputs.max_turn_cache_misses }}")
assert.Contains(t, string(lockContent), "${GH_AW_MAX_TURN_CACHE_MISSES}")
}

// TestSetupEngineAndImports_ImportedEngineVersionDefault verifies that a shared/imported
// engine definition's top-level `version` field is applied as the default
// EngineConfig.Version when the workflow's own `engine:` frontmatter selects the same
Expand Down
Loading