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
6 changes: 6 additions & 0 deletions cookbook/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
github.com/github/copilot-sdk/go v0.1.18 h1:S1ocOfTKxiNGtj+/qp4z+RZeOr9hniqy3UqIIYZxsuQ=
github.com/github/copilot-sdk/go v0.1.18/go.mod h1:0SYT+64k347IDT0Trn4JHVFlUhPtGSE6ab479tU/+tY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
2 changes: 2 additions & 0 deletions go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ func main() {
- `CreateSession(config *SessionConfig) (*Session, error)` - Create a new session
- `ResumeSession(sessionID string) (*Session, error)` - Resume an existing session
- `ResumeSessionWithOptions(sessionID string, config *ResumeSessionConfig) (*Session, error)` - Resume with additional configuration
- `ListSessions() ([]SessionMetadata, error)` - List all sessions known to the server
- `DeleteSession(sessionID string) error` - Delete a session permanently
- `GetState() ConnectionState` - Get connection state
- `Ping(message string) (*PingResponse, error)` - Ping the server

Expand Down
101 changes: 101 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,107 @@ func (c *Client) ResumeSessionWithOptions(sessionID string, config *ResumeSessio
return session, nil
}

// ListSessions returns metadata about all sessions known to the server.
//
// Returns a list of SessionMetadata for all available sessions, including their IDs,
// timestamps, and optional summaries.
//
// Example:
//
// sessions, err := client.ListSessions()
// if err != nil {
// log.Fatal(err)
// }
// for _, session := range sessions {
// fmt.Printf("Session: %s\n", session.SessionID)
// }
func (c *Client) ListSessions() ([]SessionMetadata, error) {
if c.client == nil {
if c.autoStart {
if err := c.Start(); err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("client not connected. Call Start() first")
}
}

result, err := c.client.Request("session.list", map[string]interface{}{})
if err != nil {
return nil, err
}

// Marshal and unmarshal to convert map to struct
jsonBytes, err := json.Marshal(result)
if err != nil {
return nil, fmt.Errorf("failed to marshal sessions response: %w", err)
}

var response ListSessionsResponse
if err := json.Unmarshal(jsonBytes, &response); err != nil {
return nil, fmt.Errorf("failed to unmarshal sessions response: %w", err)
}

return response.Sessions, nil
}

// DeleteSession permanently deletes a session and all its conversation history.
//
// The session cannot be resumed after deletion. If the session is in the local
// sessions map, it will be removed.
//
// Example:
//
// if err := client.DeleteSession("session-123"); err != nil {
// log.Fatal(err)
// }
func (c *Client) DeleteSession(sessionID string) error {
if c.client == nil {
if c.autoStart {
if err := c.Start(); err != nil {
return err
}
} else {
return fmt.Errorf("client not connected. Call Start() first")
}
}

params := map[string]interface{}{
"sessionId": sessionID,
}

result, err := c.client.Request("session.delete", params)
if err != nil {
return err
}

// Marshal and unmarshal to convert map to struct
jsonBytes, err := json.Marshal(result)
if err != nil {
return fmt.Errorf("failed to marshal delete response: %w", err)
}

var response DeleteSessionResponse
if err := json.Unmarshal(jsonBytes, &response); err != nil {
return fmt.Errorf("failed to unmarshal delete response: %w", err)
}

if !response.Success {
errorMsg := "unknown error"
if response.Error != nil {
errorMsg = *response.Error
}
return fmt.Errorf("failed to delete session %s: %s", sessionID, errorMsg)
}

// Remove from local sessions map if present
c.sessionsMux.Lock()
delete(c.sessions, sessionID)
c.sessionsMux.Unlock()

return nil
}

// GetState returns the current connection state of the client.
//
// Possible states: StateDisconnected, StateConnecting, StateConnected, StateError.
Expand Down
129 changes: 129 additions & 0 deletions go/e2e/session_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,135 @@ func TestSession(t *testing.T) {
t.Errorf("Expected assistant message to contain '2', got %v", assistantMessage.Data.Content)
}
})

t.Run("should list sessions", func(t *testing.T) {
ctx.ConfigureForTest(t)

// Create a couple of sessions and send messages to persist them
session1, err := client.CreateSession(nil)
if err != nil {
t.Fatalf("Failed to create session1: %v", err)
}

_, err = session1.SendAndWait(copilot.MessageOptions{Prompt: "Say hello"}, 60*time.Second)
if err != nil {
t.Fatalf("Failed to send message to session1: %v", err)
}

session2, err := client.CreateSession(nil)
if err != nil {
t.Fatalf("Failed to create session2: %v", err)
}

_, err = session2.SendAndWait(copilot.MessageOptions{Prompt: "Say goodbye"}, 60*time.Second)
if err != nil {
t.Fatalf("Failed to send message to session2: %v", err)
}

// Small delay to ensure session files are written to disk
time.Sleep(200 * time.Millisecond)

// List sessions and verify they're included
sessions, err := client.ListSessions()
if err != nil {
t.Fatalf("Failed to list sessions: %v", err)
}

// Verify it's a list
if sessions == nil {
t.Fatal("Expected sessions to be non-nil")
}

// Extract session IDs
sessionIDs := make([]string, len(sessions))
for i, s := range sessions {
sessionIDs[i] = s.SessionID
}

// Verify both sessions are in the list
if !contains(sessionIDs, session1.SessionID) {
t.Errorf("Expected session1 ID %s to be in sessions list", session1.SessionID)
}
if !contains(sessionIDs, session2.SessionID) {
t.Errorf("Expected session2 ID %s to be in sessions list", session2.SessionID)
}

// Verify session metadata structure
for _, sessionData := range sessions {
if sessionData.SessionID == "" {
t.Error("Expected sessionId to be non-empty")
}
if sessionData.StartTime == "" {
t.Error("Expected startTime to be non-empty")
}
if sessionData.ModifiedTime == "" {
t.Error("Expected modifiedTime to be non-empty")
}
// isRemote is a boolean, so it's always set
}
})

t.Run("should delete session", func(t *testing.T) {
ctx.ConfigureForTest(t)

// Create a session and send a message to persist it
session, err := client.CreateSession(nil)
if err != nil {
t.Fatalf("Failed to create session: %v", err)
}

_, err = session.SendAndWait(copilot.MessageOptions{Prompt: "Hello"}, 60*time.Second)
if err != nil {
t.Fatalf("Failed to send message: %v", err)
}

sessionID := session.SessionID

// Small delay to ensure session file is written to disk
time.Sleep(200 * time.Millisecond)

// Verify session exists in the list
sessions, err := client.ListSessions()
if err != nil {
t.Fatalf("Failed to list sessions: %v", err)
}

sessionIDs := make([]string, len(sessions))
for i, s := range sessions {
sessionIDs[i] = s.SessionID
}

if !contains(sessionIDs, sessionID) {
t.Errorf("Expected session ID %s to be in sessions list before delete", sessionID)
}

// Delete the session
err = client.DeleteSession(sessionID)
if err != nil {
t.Fatalf("Failed to delete session: %v", err)
}

// Verify session no longer exists in the list
sessionsAfter, err := client.ListSessions()
if err != nil {
t.Fatalf("Failed to list sessions after delete: %v", err)
}

sessionIDsAfter := make([]string, len(sessionsAfter))
for i, s := range sessionsAfter {
sessionIDsAfter[i] = s.SessionID
}

if contains(sessionIDsAfter, sessionID) {
t.Errorf("Expected session ID %s to NOT be in sessions list after delete", sessionID)
}

// Verify we cannot resume the deleted session
_, err = client.ResumeSession(sessionID)
if err == nil {
t.Error("Expected error when resuming deleted session")
}
})
}

func getSystemMessage(exchange testharness.ParsedHttpExchange) string {
Expand Down
25 changes: 25 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,28 @@ type ModelInfo struct {
type GetModelsResponse struct {
Models []ModelInfo `json:"models"`
}

// SessionMetadata contains metadata about a session
type SessionMetadata struct {
SessionID string `json:"sessionId"`
StartTime string `json:"startTime"`
ModifiedTime string `json:"modifiedTime"`
Summary *string `json:"summary,omitempty"`
IsRemote bool `json:"isRemote"`
}

// ListSessionsResponse is the response from session.list
type ListSessionsResponse struct {
Sessions []SessionMetadata `json:"sessions"`
}

// DeleteSessionRequest is the request for session.delete
type DeleteSessionRequest struct {
SessionID string `json:"sessionId"`
}

Comment on lines +393 to +397
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DeleteSessionRequest type is defined but never used in the codebase. The DeleteSession method builds the request parameters inline at line 795-797 in client.go, which is consistent with other methods in the SDK. Consider removing this unused type definition to reduce code clutter, or use it in the DeleteSession implementation if it was intended for consistency with other request types.

Suggested change
// DeleteSessionRequest is the request for session.delete
type DeleteSessionRequest struct {
SessionID string `json:"sessionId"`
}

Copilot uses AI. Check for mistakes.
// DeleteSessionResponse is the response from session.delete
type DeleteSessionResponse struct {
Success bool `json:"success"`
Error *string `json:"error,omitempty"`
}
6 changes: 6 additions & 0 deletions test/package-lock.json

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

Loading