diff --git a/.changeset/patch-assign-to-agent-reasoning-effort.md b/.changeset/patch-assign-to-agent-reasoning-effort.md new file mode 100644 index 00000000000..405e4129ddc --- /dev/null +++ b/.changeset/patch-assign-to-agent-reasoning-effort.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Add optional `reasoning-effort` enum forwarding to the `assign-to-agent` safe output. diff --git a/actions/setup/js/assign_agent_helpers.cjs b/actions/setup/js/assign_agent_helpers.cjs index dcf2c838275..66793c44ee8 100644 --- a/actions/setup/js/assign_agent_helpers.cjs +++ b/actions/setup/js/assign_agent_helpers.cjs @@ -36,6 +36,19 @@ function normalizeLogin(login) { */ const AGENT_NAME_BY_LOGIN = Object.fromEntries(Object.entries(AGENT_LOGIN_NAMES).flatMap(([agentName, logins]) => logins.map(login => [normalizeLogin(login), agentName]))); +const REASONING_EFFORT_VALUES = new Set(["none", "minimal", "low", "medium", "high", "xhigh"]); + +function resolveReasoningEffort(reasoningEffort) { + if (reasoningEffort == null) return null; + + const normalizedEffort = typeof reasoningEffort === "string" ? reasoningEffort.trim().toLowerCase() : null; + if (normalizedEffort == null || !REASONING_EFFORT_VALUES.has(normalizedEffort)) { + core.warning(`Ignoring reasoning-effort: expected one of ${[...REASONING_EFFORT_VALUES].join(", ")}.`); + return null; + } + return normalizedEffort; +} + /** * GitHub can surface bots either via type="Bot" or a [bot] login suffix. * Check both because assignee responses are not always consistent across endpoints. @@ -335,6 +348,7 @@ async function getPullRequestDetails(owner, repo, pullNumber, githubClient = git * @param {string|null} [pullRequestRepoSlug] - Optional pull request repository slug (owner/repo) for REST path * @param {{rationale?: string, confidence?: "LOW"|"MEDIUM"|"HIGH", suggest?: boolean}} [intentMetadata] - Optional issue-intent metadata * @param {boolean} [useIssueIntent] - Whether to include issue-intent metadata/headers + * @param {string|null} [reasoningEffort] - Optional reasoning effort * @returns {Promise} True if successful */ async function assignAgentToIssue( @@ -351,7 +365,8 @@ async function assignAgentToIssue( taskContext = null, pullRequestRepoSlug = null, intentMetadata = {}, - useIssueIntent = true + useIssueIntent = true, + reasoningEffort = null ) { // SECURITY: pullRequestRepoSlug specifies a cross-repo target repository slug. // Callers MUST validate the corresponding repository slug against allowedRepos using @@ -409,6 +424,8 @@ async function assignAgentToIssue( if (customInstructions != null) agentAssignment.custom_instructions = customInstructions; if (customAgent != null) agentAssignment.custom_agent = customAgent; if (model != null) agentAssignment.model = model; + const validReasoningEffort = resolveReasoningEffort(reasoningEffort); + if (validReasoningEffort != null) agentAssignment.reasoning_effort = validReasoningEffort; if (Object.keys(agentAssignment).length > 0) assignParams.agent_assignment = agentAssignment; await githubClient.request("POST /repos/{owner}/{repo}/issues/{issue_number}/assignees", assignParams); return true; @@ -525,6 +542,7 @@ module.exports = { getIssueDetails, getPullRequestDetails, assignAgentToIssue, + resolveReasoningEffort, logPermissionError, generatePermissionErrorSummary, assignAgentToIssueByName, diff --git a/actions/setup/js/assign_agent_helpers.test.cjs b/actions/setup/js/assign_agent_helpers.test.cjs index 84c5784092f..908d7b3a217 100644 --- a/actions/setup/js/assign_agent_helpers.test.cjs +++ b/actions/setup/js/assign_agent_helpers.test.cjs @@ -33,8 +33,20 @@ const mockGithub = { globalThis.core = mockCore; globalThis.github = mockGithub; -const { AGENT_LOGIN_NAMES, getAgentName, getAgentLogins, getAvailableAgentLogins, getAssignableBots, findAgent, getIssueDetails, getPullRequestDetails, assignAgentToIssue, generatePermissionErrorSummary, assignAgentToIssueByName } = - await import("./assign_agent_helpers.cjs"); +const { + AGENT_LOGIN_NAMES, + getAgentName, + getAgentLogins, + getAvailableAgentLogins, + getAssignableBots, + findAgent, + getIssueDetails, + getPullRequestDetails, + assignAgentToIssue, + resolveReasoningEffort, + generatePermissionErrorSummary, + assignAgentToIssueByName, +} = await import("./assign_agent_helpers.cjs"); describe("assign_agent_helpers.cjs", () => { const originalPromptsDir = process.env.GH_AW_PROMPTS_DIR; @@ -492,6 +504,60 @@ describe("assign_agent_helpers.cjs", () => { }); }); + it("should include supported reasoning effort while preserving existing fields", async () => { + const mockRequest = vi.fn().mockResolvedValue({ status: 201 }); + const restClient = { request: mockRequest }; + + await assignAgentToIssue("id", "copilot-swe-agent[bot]", [], "copilot", null, "future-model", "my-agent", "Follow the guidelines.", "main", restClient, taskContext, "otherorg/otherrepo", {}, true, "high"); + + expect(mockRequest).toHaveBeenCalledWith( + "POST /repos/{owner}/{repo}/issues/{issue_number}/assignees", + expect.objectContaining({ + agent_assignment: { + target_repo: "otherorg/otherrepo", + base_branch: "main", + custom_instructions: "Follow the guidelines.", + custom_agent: "my-agent", + model: "future-model", + reasoning_effort: "high", + }, + }) + ); + }); + + it.each(["none", "minimal", "low", "medium", "high", "xhigh"])("should support the %s reasoning-effort enum value", effort => { + expect(resolveReasoningEffort(effort)).toBe(effort); + expect(mockCore.warning).not.toHaveBeenCalled(); + }); + + it.each([ + ["unsupported value", "extreme"], + ["empty value", ""], + ["non-string value", 42], + ])("should warn and omit reasoning-effort for %s", async (_case, effort) => { + expect(resolveReasoningEffort(effort)).toBeNull(); + expect(mockCore.warning).toHaveBeenCalledOnce(); + }); + + it("should forward reasoning effort without enforcing model capabilities", async () => { + const mockRequest = vi.fn().mockResolvedValue({ status: 201 }); + const restClient = { request: mockRequest }; + + await assignAgentToIssue("id", "copilot-swe-agent[bot]", [], "copilot", null, "claude-opus-4.6", null, null, "main", restClient, taskContext, null, {}, true, "high"); + + expect(mockRequest).toHaveBeenCalledWith( + "POST /repos/{owner}/{repo}/issues/{issue_number}/assignees", + expect.objectContaining({ + agent_assignment: { + base_branch: "main", + model: "claude-opus-4.6", + reasoning_effort: "high", + }, + }) + ); + expect(mockCore.warning).not.toHaveBeenCalled(); + }); + it("should include agent_assignment with only the provided fields", async () => { const mockRequest = vi.fn().mockResolvedValue({ status: 201 }); const restClient = { request: mockRequest }; diff --git a/actions/setup/js/assign_to_agent.cjs b/actions/setup/js/assign_to_agent.cjs index 4bf658534a9..83e203d4ab0 100644 --- a/actions/setup/js/assign_to_agent.cjs +++ b/actions/setup/js/assign_to_agent.cjs @@ -1,7 +1,7 @@ // @ts-check /// -const { AGENT_LOGIN_NAMES, getAgentLogins, getAvailableAgentLogins, findAgent, getIssueDetails, getPullRequestDetails, assignAgentToIssue, generatePermissionErrorSummary } = require("./assign_agent_helpers.cjs"); +const { AGENT_LOGIN_NAMES, getAgentLogins, getAvailableAgentLogins, findAgent, getIssueDetails, getPullRequestDetails, assignAgentToIssue, generatePermissionErrorSummary, resolveReasoningEffort } = require("./assign_agent_helpers.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { resolveTarget, isStagedMode } = require("./safe_output_helpers.cjs"); const { generateStagedPreview } = require("./staged_preview.cjs"); @@ -153,6 +153,7 @@ async function main(config = {}) { } const defaultAgent = String(config.name ?? "copilot").trim(); const defaultModel = config.model ? String(config.model).trim() : null; + const reasoningEffort = config["reasoning-effort"] ?? null; const defaultCustomAgent = config["custom-agent"] ? String(config["custom-agent"]).trim() : null; const defaultCustomInstructions = config["custom-instructions"] ? String(config["custom-instructions"]).trim() : null; const configuredBaseBranch = config["base-branch"] ? String(config["base-branch"]).trim() : null; @@ -251,6 +252,8 @@ async function main(config = {}) { } parts.push(`**Agent:** ${item.agent || defaultAgent}`); if (defaultModel) parts.push(`**Model:** ${defaultModel}`); + const stagedReasoningEffort = resolveReasoningEffort(reasoningEffort); + if (stagedReasoningEffort) parts.push(`**Reasoning Effort:** ${stagedReasoningEffort}`); if (defaultCustomAgent) parts.push(`**Custom Agent:** ${defaultCustomAgent}`); if (defaultCustomInstructions) parts.push(`**Custom Instructions:** ${defaultCustomInstructions}`); return parts.join("\n") + "\n\n"; @@ -496,7 +499,8 @@ async function main(config = {}) { taskContext, effectivePullRequestRepoSlug, intentMetadata, - issueIntentEnabled + issueIntentEnabled, + reasoningEffort ); if (!success) throw new Error(`Failed to assign ${agentName} via REST`); diff --git a/actions/setup/js/assign_to_agent.test.cjs b/actions/setup/js/assign_to_agent.test.cjs index 030c003db2b..182dd216b03 100644 --- a/actions/setup/js/assign_to_agent.test.cjs +++ b/actions/setup/js/assign_to_agent.test.cjs @@ -59,6 +59,7 @@ describe("assign_to_agent", () => { const STANDALONE_RUNNER = ` const _config = {}; if (process.env.GH_AW_AGENT_DEFAULT?.trim()) _config.name = process.env.GH_AW_AGENT_DEFAULT.trim(); + if (process.env.GH_AW_AGENT_MODEL?.trim()) _config.model = process.env.GH_AW_AGENT_MODEL.trim(); if (process.env.GH_AW_AGENT_MAX_COUNT?.trim()) _config.max = process.env.GH_AW_AGENT_MAX_COUNT.trim(); if (process.env.GH_AW_AGENT_TARGET?.trim()) _config.target = process.env.GH_AW_AGENT_TARGET.trim(); if (process.env.GH_AW_AGENT_ALLOWED?.trim()) _config.allowed = process.env.GH_AW_AGENT_ALLOWED.trim(); @@ -66,6 +67,7 @@ describe("assign_to_agent", () => { if (process.env.GH_AW_AGENT_PULL_REQUEST_REPO?.trim()) _config["pull-request-repo"] = process.env.GH_AW_AGENT_PULL_REQUEST_REPO.trim(); if (process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS?.trim()) _config["allowed-pull-request-repos"] = process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS.trim(); if (process.env.GH_AW_AGENT_BASE_BRANCH?.trim()) _config["base-branch"] = process.env.GH_AW_AGENT_BASE_BRANCH.trim(); + if (process.env.GH_AW_AGENT_REASONING_EFFORT != null) _config["reasoning-effort"] = process.env.GH_AW_AGENT_REASONING_EFFORT; if (process.env.GH_AW_ALLOWED_REPOS?.trim()) _config.allowed_repos = process.env.GH_AW_ALLOWED_REPOS.trim(); let _handler; @@ -121,6 +123,7 @@ describe("assign_to_agent", () => { delete process.env.GH_AW_AGENT_OUTPUT; delete process.env.GH_AW_SAFE_OUTPUTS_STAGED; delete process.env.GH_AW_AGENT_DEFAULT; + delete process.env.GH_AW_AGENT_MODEL; delete process.env.GH_AW_AGENT_MAX_COUNT; delete process.env.GH_AW_AGENT_TARGET; delete process.env.GH_AW_AGENT_ALLOWED; @@ -131,6 +134,7 @@ describe("assign_to_agent", () => { delete process.env.GH_AW_AGENT_PULL_REQUEST_REPO; delete process.env.GH_AW_AGENT_ALLOWED_PULL_REQUEST_REPOS; delete process.env.GH_AW_AGENT_BASE_BRANCH; + delete process.env.GH_AW_AGENT_REASONING_EFFORT; // Reset context to default mockContext.eventName = "issues"; @@ -190,6 +194,17 @@ describe("assign_to_agent", () => { expect(summaryCall).toContain("Agent:** copilot"); }); + it("should include configured reasoning effort in staged previews", async () => { + process.env.GH_AW_SAFE_OUTPUTS_STAGED = "true"; + process.env.GH_AW_AGENT_REASONING_EFFORT = "high"; + process.env.GH_AW_AGENT_MODEL = "o3"; + setAgentOutput({ items: [{ type: "assign_to_agent", pull_number: 42, agent: "copilot" }], errors: [] }); + + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); + + expect(mockCore.summary.addRaw.mock.calls[0][0]).toContain("Reasoning Effort:** high"); + }); + it("should use default agent when not specified", async () => { process.env.GH_AW_AGENT_DEFAULT = "copilot"; setAgentOutput({ @@ -215,6 +230,27 @@ describe("assign_to_agent", () => { expect(mockCore.info).toHaveBeenCalledWith("Default agent: copilot"); }); + it("should forward reasoning effort for issue assignments", async () => { + process.env.GH_AW_AGENT_MODEL = "o3"; + process.env.GH_AW_AGENT_REASONING_EFFORT = "high"; + setAgentOutput({ items: [{ type: "assign_to_agent", issue_number: 42, agent: "copilot" }], errors: [] }); + mockGithub.rest.issues.get.mockResolvedValueOnce({ + data: { id: 12345, number: 42, assignees: [], html_url: "", title: "", body: "" }, + }); + + await eval(`(async () => { ${assignToAgentScript}; ${STANDALONE_RUNNER} })()`); + + expect(mockGithub.request).toHaveBeenLastCalledWith( + "POST /repos/{owner}/{repo}/issues/{issue_number}/assignees", + expect.objectContaining({ + agent_assignment: expect.objectContaining({ + model: "o3", + reasoning_effort: "high", + }), + }) + ); + }); + it("should respect max count configuration", async () => { process.env.GH_AW_AGENT_MAX_COUNT = "2"; setAgentOutput({ @@ -797,11 +833,10 @@ describe("assign_to_agent", () => { expect(summaryCall).toContain("Permission Requirements"); }); - it.skip("should handle pull_number parameter", async () => { - // TODO: Fix test mocking - the code works but the test setup has issues with GraphQL mocking for PR queries - // The functionality is identical to issue_number (just uses pullRequest instead of issue in the GraphQL query) - // and the schema/validation changes have been tested via the other validation tests + it("should forward reasoning effort for pull request assignments", async () => { process.env.GH_AW_AGENT_DEFAULT = "copilot"; + process.env.GH_AW_AGENT_MODEL = "o3"; + process.env.GH_AW_AGENT_REASONING_EFFORT = "high"; setAgentOutput({ items: [ { @@ -827,6 +862,15 @@ describe("assign_to_agent", () => { } expect(mockCore.info).toHaveBeenCalledWith(expect.stringContaining("Successfully assigned copilot coding agent to pull request #123")); + expect(mockGithub.request).toHaveBeenLastCalledWith( + "POST /repos/{owner}/{repo}/issues/{issue_number}/assignees", + expect.objectContaining({ + agent_assignment: expect.objectContaining({ + model: "o3", + reasoning_effort: "high", + }), + }) + ); expect(mockCore.setFailed).not.toHaveBeenCalled(); }); diff --git a/docs/src/content/docs/reference/copilot-cloud-agent.mdx b/docs/src/content/docs/reference/copilot-cloud-agent.mdx index 4db9b6ebd39..6b8b465eb36 100644 --- a/docs/src/content/docs/reference/copilot-cloud-agent.mdx +++ b/docs/src/content/docs/reference/copilot-cloud-agent.mdx @@ -50,7 +50,8 @@ If you're creating new issues and want to assign Copilot immediately, use `assig safe-outputs: assign-to-agent: name: "copilot" # default agent (default: "copilot") - model: "claude-opus-4.6" # default AI model (default: "auto") + model: "o3" # default AI model (default: "auto") + reasoning-effort: "high" # optional reasoning effort custom-agent: "agent-id" # default custom agent ID (optional) custom-instructions: "..." # default custom instructions (optional) allowed: [copilot] # restrict to specific agents (optional) @@ -65,6 +66,15 @@ safe-outputs: **Supported agents:** `copilot` (`copilot-swe-agent`) +`reasoning-effort` accepts `none`, `minimal`, `low`, `medium`, `high`, or `xhigh` and is forwarded as `agent_assignment.reasoning_effort` without enforcing model-specific capabilities. Invalid values produce a warning and are omitted without failing the assignment. The field also supports expressions: + +```yaml wrap +safe-outputs: + assign-to-agent: + model: "o3" + reasoning-effort: ${{ inputs.reasoning_effort }} +``` + ### Target Issue or Pull Request The `target` parameter determines which issue or PR to assign the agent to: diff --git a/docs/src/content/docs/reference/frontmatter-full.md b/docs/src/content/docs/reference/frontmatter-full.md index ba7db0ed898..ca289e7d1cd 100644 --- a/docs/src/content/docs/reference/frontmatter-full.md +++ b/docs/src/content/docs/reference/frontmatter-full.md @@ -13862,6 +13862,12 @@ safe-outputs: # (optional) model: "example-value" + # Optional reasoning effort to forward without model-specific capability checks. + # Supports none, minimal, low, medium, high, xhigh, and GitHub Actions + # expressions. Invalid runtime values are ignored with a warning. + # (optional) + reasoning-effort: "example-value" + # Default custom agent ID to use when assigning custom agents. This is used for # specialized agent configurations beyond the standard Copilot agent. # (optional) diff --git a/docs/src/content/docs/reference/safe-outputs.md b/docs/src/content/docs/reference/safe-outputs.md index 780b528c783..aae94ac7923 100644 --- a/docs/src/content/docs/reference/safe-outputs.md +++ b/docs/src/content/docs/reference/safe-outputs.md @@ -1607,7 +1607,8 @@ Programmatically assigns GitHub Copilot coding agent to **existing** issues or p safe-outputs: assign-to-agent: name: "copilot" # default agent (default: "copilot") - model: "claude-sonnet-5" # default AI model (default: "auto") + model: "o3" # default AI model (default: "auto") + reasoning-effort: "high" # optional enum value or expression custom-agent: "agent-id" # default custom agent ID (optional) custom-instructions: "..." # default custom instructions (optional) allowed: [copilot] # restrict to specific agents (optional) @@ -1620,6 +1621,8 @@ safe-outputs: github-token: ${{ secrets.SOME_CUSTOM_TOKEN }} # optional custom token for permissions ``` +`reasoning-effort` accepts `none`, `minimal`, `low`, `medium`, `high`, or `xhigh`, as well as GitHub Actions expressions. Valid values are forwarded without enforcing model-specific capabilities; invalid runtime values produce a warning and are omitted without failing assignment. + See **[Copilot Cloud Agent](/gh-aw/reference/copilot-cloud-agent/#assign-to-agent)** for complete configuration options and authorization setup. If you're creating new issues and want to assign an agent immediately, use `assignees: copilot` in your [`create-issue`](#issue-creation-create-issue) configuration instead. diff --git a/pkg/cli/workflows/test-assign-to-agent-with-dynamic-reasoning-effort.md b/pkg/cli/workflows/test-assign-to-agent-with-dynamic-reasoning-effort.md new file mode 100644 index 00000000000..8934abd3448 --- /dev/null +++ b/pkg/cli/workflows/test-assign-to-agent-with-dynamic-reasoning-effort.md @@ -0,0 +1,24 @@ +--- +name: Test Assign to Agent with Dynamic Reasoning Effort +on: + workflow_dispatch: + inputs: + reasoning_effort: + description: Reasoning effort to use + required: true + type: string +permissions: + contents: read + issues: read +engine: copilot +safe-outputs: + assign-to-agent: + name: copilot + model: o3 + reasoning-effort: ${{ inputs.reasoning_effort }} +strict: false +--- + +# Test Dynamic Assign to Agent Reasoning Effort + +Assign issue #1 to Copilot using the configured reasoning effort. diff --git a/pkg/cli/workflows/test-assign-to-agent-with-model.md b/pkg/cli/workflows/test-assign-to-agent-with-model.md index c79aa490ec0..7a12e340de9 100644 --- a/pkg/cli/workflows/test-assign-to-agent-with-model.md +++ b/pkg/cli/workflows/test-assign-to-agent-with-model.md @@ -42,6 +42,7 @@ safe-outputs: max: 5 name: copilot model: claude-sonnet-5 # Default model to use when not specified per-item + reasoning-effort: high # Forwarded independently of the selected model target: "triggering" # Auto-resolves from workflow context (default) allowed: [copilot] # Only allow copilot agent strict: false diff --git a/pkg/cli/workflows/test-copilot-assign-to-agent.md b/pkg/cli/workflows/test-copilot-assign-to-agent.md index 2b0ea74bd49..ab033500980 100644 --- a/pkg/cli/workflows/test-copilot-assign-to-agent.md +++ b/pkg/cli/workflows/test-copilot-assign-to-agent.md @@ -8,6 +8,8 @@ engine: copilot safe-outputs: assign-to-agent: max: 1 + model: o3 + reasoning-effort: high --- # Test Copilot Assign To Agent diff --git a/pkg/parser/schemas/main_workflow_schema.json b/pkg/parser/schemas/main_workflow_schema.json index a164d8c6973..bb845c8b755 100644 --- a/pkg/parser/schemas/main_workflow_schema.json +++ b/pkg/parser/schemas/main_workflow_schema.json @@ -9593,6 +9593,11 @@ "type": "string", "description": "Default AI model to use for the agent (e.g., 'auto', 'claude-sonnet-4.5', 'claude-opus-4.5', 'claude-opus-4.6', 'gpt-5.1-codex-max', 'gpt-5.2-codex'). Defaults to 'auto' if not specified." }, + "reasoning-effort": { + "type": "string", + "description": "Optional reasoning effort to forward without model-specific capability checks. Supports none, minimal, low, medium, high, xhigh, and GitHub Actions expressions. Invalid runtime values are ignored with a warning.", + "examples": ["high", "${{ inputs.reasoning_effort }}"] + }, "custom-agent": { "type": "string", "description": "Default custom agent ID to use when assigning custom agents. This is used for specialized agent configurations beyond the standard Copilot agent." diff --git a/pkg/workflow/assign_to_agent.go b/pkg/workflow/assign_to_agent.go index 5a661c0be59..d7266b566fc 100644 --- a/pkg/workflow/assign_to_agent.go +++ b/pkg/workflow/assign_to_agent.go @@ -12,6 +12,7 @@ type AssignToAgentConfig struct { SafeOutputTargetConfig `yaml:",inline"` DefaultAgent string `yaml:"name,omitempty"` // Default agent to assign (e.g., "copilot") DefaultModel string `yaml:"model,omitempty"` // Default AI model to use (e.g., "claude-sonnet-5") + ReasoningEffort string `yaml:"reasoning-effort,omitempty"` // Optional reasoning effort DefaultCustomAgent string `yaml:"custom-agent,omitempty"` // Default custom agent ID for custom agents DefaultCustomInstructions string `yaml:"custom-instructions,omitempty"` // Default custom instructions for the agent Allowed []string `yaml:"allowed,omitempty"` // Optional list of allowed agent names. If omitted, any agents are allowed. diff --git a/pkg/workflow/assign_to_agent_test.go b/pkg/workflow/assign_to_agent_test.go index 78d39c191ce..e56451e298e 100644 --- a/pkg/workflow/assign_to_agent_test.go +++ b/pkg/workflow/assign_to_agent_test.go @@ -43,6 +43,62 @@ This workflow tests canonical 'name' key. assert.Equal(t, "copilot", workflowData.SafeOutputs.AssignToAgent.DefaultAgent, "Should parse 'name' key as DefaultAgent") } +func TestAssignToAgentReasoningEffort(t *testing.T) { + tests := []struct { + name string + value string + }{ + {name: "literal", value: "high"}, + {name: "model specific", value: "xhigh"}, + {name: "expression", value: "${{ inputs.reasoning_effort }}"}, + {name: "empty", value: ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := testutil.TempDir(t, "assign-to-agent-reasoning-effort") + workflow := `--- +on: workflow_dispatch +engine: copilot +permissions: + contents: read +safe-outputs: + assign-to-agent: + reasoning-effort: "` + tt.value + `" +--- +# Test Workflow +` + testFile := filepath.Join(tmpDir, "test-assign-to-agent.md") + require.NoError(t, os.WriteFile(testFile, []byte(workflow), 0644)) + + workflowData, err := NewCompiler(WithVersion("1.0.0")).ParseWorkflowFile(testFile) + require.NoError(t, err) + require.NotNil(t, workflowData.SafeOutputs.AssignToAgent) + assert.Equal(t, tt.value, workflowData.SafeOutputs.AssignToAgent.ReasoningEffort) + }) + } +} + +func TestAssignToAgentReasoningEffortRejectsNonString(t *testing.T) { + tmpDir := testutil.TempDir(t, "assign-to-agent-reasoning-effort-invalid") + workflow := `--- +on: issues +engine: copilot +permissions: + contents: read +safe-outputs: + assign-to-agent: + reasoning-effort: 42 +--- +# Test Workflow +` + testFile := filepath.Join(tmpDir, "test-assign-to-agent.md") + require.NoError(t, os.WriteFile(testFile, []byte(workflow), 0644)) + + _, err := NewCompiler(WithVersion("1.0.0")).ParseWorkflowFile(testFile) + require.Error(t, err) +} + // TestAssignToAgentInHandlerManagerStep verifies that assign_to_agent is processed within // the handler manager step (process_safe_outputs) and that the safe_outputs job exports // the required assign_to_agent outputs for the conclusion job. diff --git a/pkg/workflow/safe_outputs_handler_registry_assignments.go b/pkg/workflow/safe_outputs_handler_registry_assignments.go index 252da5cf785..8ed4c0a2356 100644 --- a/pkg/workflow/safe_outputs_handler_registry_assignments.go +++ b/pkg/workflow/safe_outputs_handler_registry_assignments.go @@ -11,6 +11,7 @@ var assignmentHandlerRegistry = map[string]handlerBuilder{ AddTemplatableInt("max", c.Max). AddIfNotEmpty("name", c.DefaultAgent). AddIfNotEmpty("model", c.DefaultModel). + AddIfNotEmpty("reasoning-effort", c.ReasoningEffort). AddIfNotEmpty("custom-agent", c.DefaultCustomAgent). AddIfNotEmpty("custom-instructions", c.DefaultCustomInstructions). AddStringSlice("allowed", c.Allowed). diff --git a/scratchpad/safe-outputs-specification.md b/scratchpad/safe-outputs-specification.md index 610a8f64332..ebba9d0f60e 100644 --- a/scratchpad/safe-outputs-specification.md +++ b/scratchpad/safe-outputs-specification.md @@ -1159,6 +1159,8 @@ safe-outputs: assign-to-agent: target-repo: "octocat/issues" pull-request-repo: "octocat/codebase" + model: "o3" + reasoning-effort: "high" allowed-pull-request-repos: - "octocat/codebase" - "octocat/codebase-v2" @@ -1179,6 +1181,7 @@ safe-outputs: - Agent creates PR in `octocat/codebase` (not in `octocat/issues`) - GraphQL mutation includes `agentAssignment.targetRepositoryId` - Enables issue tracking separate from code repositories +- Valid `reasoning-effort` enum values are forwarded as `agent_assignment.reasoning_effort` without model-specific capability checks; invalid values warn and are omitted #### A.4 Staged Mode Preview