diff --git a/README.md b/README.md index 3a660845c..1c6677ba9 100644 --- a/README.md +++ b/README.md @@ -735,6 +735,7 @@ The following sets of tools are available: - **assign_copilot_to_issue** - Assign Copilot to issue - **Required OAuth Scopes**: `repo` - `base_ref`: Git reference (e.g., branch) that the agent will start its work from. If not specified, defaults to the repository's default branch (string, optional) + - `custom_agent`: Optional custom agent slug to use instead of the default Copilot coding agent (string, optional) - `custom_instructions`: Optional custom instructions to guide the agent beyond the issue body. Use this to provide additional context, constraints, or guidance that is not captured in the issue description (string, optional) - `issue_number`: Issue number (number, required) - `owner`: Repository owner (string, required) diff --git a/pkg/github/__toolsnaps__/assign_copilot_to_issue.snap b/pkg/github/__toolsnaps__/assign_copilot_to_issue.snap index 994d9f570..1ae995e5d 100644 --- a/pkg/github/__toolsnaps__/assign_copilot_to_issue.snap +++ b/pkg/github/__toolsnaps__/assign_copilot_to_issue.snap @@ -23,6 +23,10 @@ "description": "Git reference (e.g., branch) that the agent will start its work from. If not specified, defaults to the repository's default branch", "type": "string" }, + "custom_agent": { + "description": "Optional custom agent slug to use instead of the default Copilot coding agent", + "type": "string" + }, "custom_instructions": { "description": "Optional custom instructions to guide the agent beyond the issue body. Use this to provide additional context, constraints, or guidance that is not captured in the issue description", "type": "string" diff --git a/pkg/github/copilot.go b/pkg/github/copilot.go index 62b18350e..dc8f312b3 100644 --- a/pkg/github/copilot.go +++ b/pkg/github/copilot.go @@ -196,6 +196,10 @@ func AssignCopilotToIssue(t translations.TranslationHelperFunc) inventory.Server Type: "string", Description: "Optional custom instructions to guide the agent beyond the issue body. Use this to provide additional context, constraints, or guidance that is not captured in the issue description", }, + "custom_agent": { + Type: "string", + Description: "Optional custom agent slug to use instead of the default Copilot coding agent", + }, }, Required: []string{"owner", "repo", "issue_number"}, }, @@ -208,6 +212,7 @@ func AssignCopilotToIssue(t translations.TranslationHelperFunc) inventory.Server IssueNumber int32 `mapstructure:"issue_number"` BaseRef string `mapstructure:"base_ref"` CustomInstructions string `mapstructure:"custom_instructions"` + CustomAgent string `mapstructure:"custom_agent"` } if err := mapstructure.WeakDecode(args, ¶ms); err != nil { return utils.NewToolResultError(err.Error()), nil, nil @@ -328,6 +333,12 @@ func AssignCopilotToIssue(t translations.TranslationHelperFunc) inventory.Server agentAssignment.CustomInstructions = &customInstructions } + // Add custom agent if provided + if params.CustomAgent != "" { + customAgent := githubv4.String(params.CustomAgent) + agentAssignment.CustomAgent = &customAgent + } + // Execute the updateIssue mutation with the GraphQL-Features header // This header is required for the agent assignment API which is not GA yet var updateIssueMutation struct { diff --git a/pkg/github/copilot_test.go b/pkg/github/copilot_test.go index f52c8eecc..e9ee96452 100644 --- a/pkg/github/copilot_test.go +++ b/pkg/github/copilot_test.go @@ -32,6 +32,7 @@ func TestAssignCopilotToIssue(t *testing.T) { assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "issue_number") assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "base_ref") assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "custom_instructions") + assert.Contains(t, tool.InputSchema.(*jsonschema.Schema).Properties, "custom_agent") assert.ElementsMatch(t, tool.InputSchema.(*jsonschema.Schema).Required, []string{"owner", "repo", "issue_number"}) // Helper function to create pointer to githubv4.String @@ -802,8 +803,116 @@ func TestAssignCopilotToIssue(t *testing.T) { }), ), ), - }, - } + }, { + name: "successful assignment with custom_agent specified", + requestArgs: map[string]any{ + "owner": "owner", + "repo": "repo", + "issue_number": float64(123), + "custom_agent": "my-custom-agent", + }, + mockedClient: githubv4mock.NewMockedHTTPClient( + githubv4mock.NewQueryMatcher( + struct { + Repository struct { + SuggestedActors struct { + Nodes []struct { + Bot struct { + ID githubv4.ID + Login githubv4.String + TypeName string `graphql:"__typename"` + } `graphql:"... on Bot"` + } + PageInfo struct { + HasNextPage bool + EndCursor string + } + } `graphql:"suggestedActors(first: 100, after: $endCursor, capabilities: CAN_BE_ASSIGNED)"` + } `graphql:"repository(owner: $owner, name: $name)"` + }{}, + map[string]any{ + "owner": githubv4.String("owner"), + "name": githubv4.String("repo"), + "endCursor": (*githubv4.String)(nil), + }, + githubv4mock.DataResponse(map[string]any{ + "repository": map[string]any{ + "suggestedActors": map[string]any{ + "nodes": []any{ + map[string]any{ + "id": githubv4.ID("copilot-swe-agent-id"), + "login": githubv4.String("copilot-swe-agent"), + "__typename": "Bot", + }, + }, + }, + }, + }), + ), + githubv4mock.NewQueryMatcher( + struct { + Repository struct { + ID githubv4.ID + Issue struct { + ID githubv4.ID + Assignees struct { + Nodes []struct { + ID githubv4.ID + } + } `graphql:"assignees(first: 100)"` + } `graphql:"issue(number: $number)"` + } `graphql:"repository(owner: $owner, name: $name)"` + }{}, + map[string]any{ + "owner": githubv4.String("owner"), + "name": githubv4.String("repo"), + "number": githubv4.Int(123), + }, + githubv4mock.DataResponse(map[string]any{ + "repository": map[string]any{ + "id": githubv4.ID("test-repo-id"), + "issue": map[string]any{ + "id": githubv4.ID("test-issue-id"), + "assignees": map[string]any{ + "nodes": []any{}, + }, + }, + }, + }), + ), + githubv4mock.NewMutationMatcher( + struct { + UpdateIssue struct { + Issue struct { + ID githubv4.ID + Number githubv4.Int + URL githubv4.String + } + } `graphql:"updateIssue(input: $input)"` + }{}, + UpdateIssueInput{ + ID: githubv4.ID("test-issue-id"), + AssigneeIDs: []githubv4.ID{githubv4.ID("copilot-swe-agent-id")}, + AgentAssignment: &AgentAssignmentInput{ + BaseRef: nil, + CustomAgent: ptrGitHubv4String("my-custom-agent"), + CustomInstructions: ptrGitHubv4String(""), + TargetRepositoryID: githubv4.ID("test-repo-id"), + }, + }, + nil, + githubv4mock.DataResponse(map[string]any{ + "updateIssue": map[string]any{ + "issue": map[string]any{ + "id": githubv4.ID("test-issue-id"), + "number": githubv4.Int(123), + "url": githubv4.String("https://github.com/owner/repo/issues/123"), + }, + }, + }), + ), + ), + }} for _, tc := range tests { t.Run(tc.name, func(t *testing.T) {