fix(providers): fix GitHub Copilot provider session lifecycle and response handling

- Store client as struct field to prevent premature garbage collection
- Remove defer client.Stop() from constructor which was killing the gRPC
  client immediately after NewGitHubCopilotProvider returned, causing all
  subsequent Chat() calls to silently fail
- Add proper error handling for CreateSession (was ignoring error with _)
- Replace session.Send() with session.SendAndWait(): Send() only returns a
  message UUID, not the response text; SendAndWait() blocks until the
  assistant is idle and returns the actual SessionEvent with content
- Add Close() method for proper client lifecycle management
- Add 15s connection timeout to fail fast if CLI is unreachable
This commit is contained in:
Sai Sankar Gochhayat 2026-02-20 12:59:50 -08:00
parent e883e14b81
commit 9bf6199c5f

View file

@ -2,56 +2,70 @@ package providers
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"time"
json "encoding/json"
copilot "github.com/github/copilot-sdk/go" copilot "github.com/github/copilot-sdk/go"
) )
type GitHubCopilotProvider struct { type GitHubCopilotProvider struct {
uri string uri string
connectMode string // `stdio` or `grpc`` connectMode string // `stdio` or `grpc`
client *copilot.Client
session *copilot.Session session *copilot.Session
} }
func NewGitHubCopilotProvider(uri string, connectMode string, model string) (*GitHubCopilotProvider, error) { func NewGitHubCopilotProvider(uri string, connectMode string, model string) (*GitHubCopilotProvider, error) {
var session *copilot.Session var session *copilot.Session
var client *copilot.Client
if connectMode == "" { if connectMode == "" {
connectMode = "grpc" connectMode = "grpc"
} }
switch connectMode { switch connectMode {
case "stdio": case "stdio":
// todo //todo
case "grpc": case "grpc":
client := copilot.NewClient(&copilot.ClientOptions{ client = copilot.NewClient(&copilot.ClientOptions{
CLIUrl: uri, CLIUrl: uri,
}) })
if err := client.Start(context.Background()); err != nil { connectCtx, connectCancel := context.WithTimeout(context.Background(), 15*time.Second)
return nil, fmt.Errorf( defer connectCancel()
"Can't connect to Github Copilot, https://github.com/github/copilot-sdk/blob/main/docs/getting-started.md#connecting-to-an-external-cli-server for details", if err := client.Start(connectCtx); err != nil {
) return nil, fmt.Errorf("can't connect to Github Copilot: %w", err)
} }
defer client.Stop() var err error
session, _ = client.CreateSession(context.Background(), &copilot.SessionConfig{ session, err = client.CreateSession(connectCtx, &copilot.SessionConfig{
Model: model, Model: model,
Hooks: &copilot.SessionHooks{}, Hooks: &copilot.SessionHooks{},
}) })
if err != nil {
client.Stop()
return nil, fmt.Errorf("failed to create Copilot session: %w", err)
}
} }
return &GitHubCopilotProvider{ return &GitHubCopilotProvider{
uri: uri, uri: uri,
connectMode: connectMode, connectMode: connectMode,
client: client,
session: session, session: session,
}, nil }, nil
} }
func (p *GitHubCopilotProvider) Close() {
if p.client != nil {
p.client.Stop()
}
}
// Chat sends a chat request to GitHub Copilot // Chat sends a chat request to GitHub Copilot
func (p *GitHubCopilotProvider) Chat( func (p *GitHubCopilotProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]any,
) (*LLMResponse, error) {
type tempMessage struct { type tempMessage struct {
Role string `json:"role"` Role string `json:"role"`
Content string `json:"content"` Content string `json:"content"`
@ -67,16 +81,25 @@ func (p *GitHubCopilotProvider) Chat(
fullcontent, _ := json.Marshal(out) fullcontent, _ := json.Marshal(out)
content, _ := p.session.Send(ctx, copilot.MessageOptions{ event, err := p.session.SendAndWait(ctx, copilot.MessageOptions{
Prompt: string(fullcontent), Prompt: string(fullcontent),
}) })
if err != nil {
return nil, fmt.Errorf("copilot error: %w", err)
}
if event == nil || event.Data.Content == nil {
return nil, fmt.Errorf("empty response from Copilot")
}
return &LLMResponse{ return &LLMResponse{
FinishReason: "stop", FinishReason: "stop",
Content: content, Content: *event.Data.Content,
}, nil }, nil
} }
func (p *GitHubCopilotProvider) GetDefaultModel() string { func (p *GitHubCopilotProvider) GetDefaultModel() string {
return "gpt-4.1" return "gpt-4.1"
} }