Skip to content

Commit 21290da

Browse files
Copilotfriggeri
andauthored
Add ListSessions and DeleteSession methods to Go SDK (#213)
* Initial plan * Add ListSessions and DeleteSession methods to Go SDK Co-authored-by: friggeri <106686+friggeri@users.noreply.github.com> * Update README with ListSessions and DeleteSession documentation Co-authored-by: friggeri <106686+friggeri@users.noreply.github.com> * Add ListSessions and DeleteSession methods to Go SDK Co-authored-by: friggeri <106686+friggeri@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: friggeri <106686+friggeri@users.noreply.github.com>
1 parent db56de9 commit 21290da

6 files changed

Lines changed: 269 additions & 0 deletions

File tree

cookbook/go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
github.com/github/copilot-sdk/go v0.1.18 h1:S1ocOfTKxiNGtj+/qp4z+RZeOr9hniqy3UqIIYZxsuQ=
2+
github.com/github/copilot-sdk/go v0.1.18/go.mod h1:0SYT+64k347IDT0Trn4JHVFlUhPtGSE6ab479tU/+tY=
3+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
4+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
5+
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
6+
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=

go/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ func main() {
8080
- `CreateSession(config *SessionConfig) (*Session, error)` - Create a new session
8181
- `ResumeSession(sessionID string) (*Session, error)` - Resume an existing session
8282
- `ResumeSessionWithOptions(sessionID string, config *ResumeSessionConfig) (*Session, error)` - Resume with additional configuration
83+
- `ListSessions() ([]SessionMetadata, error)` - List all sessions known to the server
84+
- `DeleteSession(sessionID string) error` - Delete a session permanently
8385
- `GetState() ConnectionState` - Get connection state
8486
- `Ping(message string) (*PingResponse, error)` - Ping the server
8587

go/client.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,107 @@ func (c *Client) ResumeSessionWithOptions(sessionID string, config *ResumeSessio
727727
return session, nil
728728
}
729729

730+
// ListSessions returns metadata about all sessions known to the server.
731+
//
732+
// Returns a list of SessionMetadata for all available sessions, including their IDs,
733+
// timestamps, and optional summaries.
734+
//
735+
// Example:
736+
//
737+
// sessions, err := client.ListSessions()
738+
// if err != nil {
739+
// log.Fatal(err)
740+
// }
741+
// for _, session := range sessions {
742+
// fmt.Printf("Session: %s\n", session.SessionID)
743+
// }
744+
func (c *Client) ListSessions() ([]SessionMetadata, error) {
745+
if c.client == nil {
746+
if c.autoStart {
747+
if err := c.Start(); err != nil {
748+
return nil, err
749+
}
750+
} else {
751+
return nil, fmt.Errorf("client not connected. Call Start() first")
752+
}
753+
}
754+
755+
result, err := c.client.Request("session.list", map[string]interface{}{})
756+
if err != nil {
757+
return nil, err
758+
}
759+
760+
// Marshal and unmarshal to convert map to struct
761+
jsonBytes, err := json.Marshal(result)
762+
if err != nil {
763+
return nil, fmt.Errorf("failed to marshal sessions response: %w", err)
764+
}
765+
766+
var response ListSessionsResponse
767+
if err := json.Unmarshal(jsonBytes, &response); err != nil {
768+
return nil, fmt.Errorf("failed to unmarshal sessions response: %w", err)
769+
}
770+
771+
return response.Sessions, nil
772+
}
773+
774+
// DeleteSession permanently deletes a session and all its conversation history.
775+
//
776+
// The session cannot be resumed after deletion. If the session is in the local
777+
// sessions map, it will be removed.
778+
//
779+
// Example:
780+
//
781+
// if err := client.DeleteSession("session-123"); err != nil {
782+
// log.Fatal(err)
783+
// }
784+
func (c *Client) DeleteSession(sessionID string) error {
785+
if c.client == nil {
786+
if c.autoStart {
787+
if err := c.Start(); err != nil {
788+
return err
789+
}
790+
} else {
791+
return fmt.Errorf("client not connected. Call Start() first")
792+
}
793+
}
794+
795+
params := map[string]interface{}{
796+
"sessionId": sessionID,
797+
}
798+
799+
result, err := c.client.Request("session.delete", params)
800+
if err != nil {
801+
return err
802+
}
803+
804+
// Marshal and unmarshal to convert map to struct
805+
jsonBytes, err := json.Marshal(result)
806+
if err != nil {
807+
return fmt.Errorf("failed to marshal delete response: %w", err)
808+
}
809+
810+
var response DeleteSessionResponse
811+
if err := json.Unmarshal(jsonBytes, &response); err != nil {
812+
return fmt.Errorf("failed to unmarshal delete response: %w", err)
813+
}
814+
815+
if !response.Success {
816+
errorMsg := "unknown error"
817+
if response.Error != nil {
818+
errorMsg = *response.Error
819+
}
820+
return fmt.Errorf("failed to delete session %s: %s", sessionID, errorMsg)
821+
}
822+
823+
// Remove from local sessions map if present
824+
c.sessionsMux.Lock()
825+
delete(c.sessions, sessionID)
826+
c.sessionsMux.Unlock()
827+
828+
return nil
829+
}
830+
730831
// GetState returns the current connection state of the client.
731832
//
732833
// Possible states: StateDisconnected, StateConnecting, StateConnected, StateError.

go/e2e/session_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,135 @@ func TestSession(t *testing.T) {
750750
t.Errorf("Expected assistant message to contain '2', got %v", assistantMessage.Data.Content)
751751
}
752752
})
753+
754+
t.Run("should list sessions", func(t *testing.T) {
755+
ctx.ConfigureForTest(t)
756+
757+
// Create a couple of sessions and send messages to persist them
758+
session1, err := client.CreateSession(nil)
759+
if err != nil {
760+
t.Fatalf("Failed to create session1: %v", err)
761+
}
762+
763+
_, err = session1.SendAndWait(copilot.MessageOptions{Prompt: "Say hello"}, 60*time.Second)
764+
if err != nil {
765+
t.Fatalf("Failed to send message to session1: %v", err)
766+
}
767+
768+
session2, err := client.CreateSession(nil)
769+
if err != nil {
770+
t.Fatalf("Failed to create session2: %v", err)
771+
}
772+
773+
_, err = session2.SendAndWait(copilot.MessageOptions{Prompt: "Say goodbye"}, 60*time.Second)
774+
if err != nil {
775+
t.Fatalf("Failed to send message to session2: %v", err)
776+
}
777+
778+
// Small delay to ensure session files are written to disk
779+
time.Sleep(200 * time.Millisecond)
780+
781+
// List sessions and verify they're included
782+
sessions, err := client.ListSessions()
783+
if err != nil {
784+
t.Fatalf("Failed to list sessions: %v", err)
785+
}
786+
787+
// Verify it's a list
788+
if sessions == nil {
789+
t.Fatal("Expected sessions to be non-nil")
790+
}
791+
792+
// Extract session IDs
793+
sessionIDs := make([]string, len(sessions))
794+
for i, s := range sessions {
795+
sessionIDs[i] = s.SessionID
796+
}
797+
798+
// Verify both sessions are in the list
799+
if !contains(sessionIDs, session1.SessionID) {
800+
t.Errorf("Expected session1 ID %s to be in sessions list", session1.SessionID)
801+
}
802+
if !contains(sessionIDs, session2.SessionID) {
803+
t.Errorf("Expected session2 ID %s to be in sessions list", session2.SessionID)
804+
}
805+
806+
// Verify session metadata structure
807+
for _, sessionData := range sessions {
808+
if sessionData.SessionID == "" {
809+
t.Error("Expected sessionId to be non-empty")
810+
}
811+
if sessionData.StartTime == "" {
812+
t.Error("Expected startTime to be non-empty")
813+
}
814+
if sessionData.ModifiedTime == "" {
815+
t.Error("Expected modifiedTime to be non-empty")
816+
}
817+
// isRemote is a boolean, so it's always set
818+
}
819+
})
820+
821+
t.Run("should delete session", func(t *testing.T) {
822+
ctx.ConfigureForTest(t)
823+
824+
// Create a session and send a message to persist it
825+
session, err := client.CreateSession(nil)
826+
if err != nil {
827+
t.Fatalf("Failed to create session: %v", err)
828+
}
829+
830+
_, err = session.SendAndWait(copilot.MessageOptions{Prompt: "Hello"}, 60*time.Second)
831+
if err != nil {
832+
t.Fatalf("Failed to send message: %v", err)
833+
}
834+
835+
sessionID := session.SessionID
836+
837+
// Small delay to ensure session file is written to disk
838+
time.Sleep(200 * time.Millisecond)
839+
840+
// Verify session exists in the list
841+
sessions, err := client.ListSessions()
842+
if err != nil {
843+
t.Fatalf("Failed to list sessions: %v", err)
844+
}
845+
846+
sessionIDs := make([]string, len(sessions))
847+
for i, s := range sessions {
848+
sessionIDs[i] = s.SessionID
849+
}
850+
851+
if !contains(sessionIDs, sessionID) {
852+
t.Errorf("Expected session ID %s to be in sessions list before delete", sessionID)
853+
}
854+
855+
// Delete the session
856+
err = client.DeleteSession(sessionID)
857+
if err != nil {
858+
t.Fatalf("Failed to delete session: %v", err)
859+
}
860+
861+
// Verify session no longer exists in the list
862+
sessionsAfter, err := client.ListSessions()
863+
if err != nil {
864+
t.Fatalf("Failed to list sessions after delete: %v", err)
865+
}
866+
867+
sessionIDsAfter := make([]string, len(sessionsAfter))
868+
for i, s := range sessionsAfter {
869+
sessionIDsAfter[i] = s.SessionID
870+
}
871+
872+
if contains(sessionIDsAfter, sessionID) {
873+
t.Errorf("Expected session ID %s to NOT be in sessions list after delete", sessionID)
874+
}
875+
876+
// Verify we cannot resume the deleted session
877+
_, err = client.ResumeSession(sessionID)
878+
if err == nil {
879+
t.Error("Expected error when resuming deleted session")
880+
}
881+
})
753882
}
754883

755884
func getSystemMessage(exchange testharness.ParsedHttpExchange) string {

go/types.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,3 +375,28 @@ type ModelInfo struct {
375375
type GetModelsResponse struct {
376376
Models []ModelInfo `json:"models"`
377377
}
378+
379+
// SessionMetadata contains metadata about a session
380+
type SessionMetadata struct {
381+
SessionID string `json:"sessionId"`
382+
StartTime string `json:"startTime"`
383+
ModifiedTime string `json:"modifiedTime"`
384+
Summary *string `json:"summary,omitempty"`
385+
IsRemote bool `json:"isRemote"`
386+
}
387+
388+
// ListSessionsResponse is the response from session.list
389+
type ListSessionsResponse struct {
390+
Sessions []SessionMetadata `json:"sessions"`
391+
}
392+
393+
// DeleteSessionRequest is the request for session.delete
394+
type DeleteSessionRequest struct {
395+
SessionID string `json:"sessionId"`
396+
}
397+
398+
// DeleteSessionResponse is the response from session.delete
399+
type DeleteSessionResponse struct {
400+
Success bool `json:"success"`
401+
Error *string `json:"error,omitempty"`
402+
}

test/package-lock.json

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)