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-assign-to-agent-reasoning-effort.md

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

20 changes: 19 additions & 1 deletion actions/setup/js/assign_agent_helpers.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<boolean>} True if successful
*/
async function assignAgentToIssue(
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -525,6 +542,7 @@ module.exports = {
getIssueDetails,
getPullRequestDetails,
assignAgentToIssue,
resolveReasoningEffort,
logPermissionError,
generatePermissionErrorSummary,
assignAgentToIssueByName,
Expand Down
70 changes: 68 additions & 2 deletions actions/setup/js/assign_agent_helpers.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 };
Expand Down
8 changes: 6 additions & 2 deletions actions/setup/js/assign_to_agent.cjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @ts-check
/// <reference types="@actions/github-script" />

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");
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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`);

Expand Down
52 changes: 48 additions & 4 deletions actions/setup/js/assign_to_agent.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,15 @@ 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();
if (process.env.GH_AW_AGENT_IGNORE_IF_ERROR?.trim()) _config["ignore-if-error"] = process.env.GH_AW_AGENT_IGNORE_IF_ERROR.trim();
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;
Expand Down Expand Up @@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -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({
Expand All @@ -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({
Expand Down Expand Up @@ -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: [
{
Expand All @@ -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();
});

Expand Down
12 changes: 11 additions & 1 deletion docs/src/content/docs/reference/copilot-cloud-agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down
6 changes: 6 additions & 0 deletions docs/src/content/docs/reference/frontmatter-full.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 4 additions & 1 deletion docs/src/content/docs/reference/safe-outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions pkg/cli/workflows/test-assign-to-agent-with-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions pkg/cli/workflows/test-copilot-assign-to-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ engine: copilot
safe-outputs:
assign-to-agent:
max: 1
model: o3
reasoning-effort: high
---

# Test Copilot Assign To Agent
Expand Down
Loading
Loading