Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions pkg/github/__toolsnaps__/assign_copilot_to_issue.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
11 changes: 11 additions & 0 deletions pkg/github/copilot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
},
Expand All @@ -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, &params); err != nil {
return utils.NewToolResultError(err.Error()), nil, nil
Expand Down Expand Up @@ -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 {
Expand Down
113 changes: 111 additions & 2 deletions pkg/github/copilot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down