Skip to content

Commit 3a00c02

Browse files
Copilotpatniko
andauthored
Add get_last_session_id() to Python and Go SDKs (github#671)
* Initial plan * Add get_last_session_id() to Python and Go SDKs with E2E tests Co-authored-by: patniko <26906478+patniko@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: patniko <26906478+patniko@users.noreply.github.com>
1 parent 8898aea commit 3a00c02

File tree

7 files changed

+128
-0
lines changed

7 files changed

+128
-0
lines changed

go/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ That's it! When your application calls `copilot.NewClient` without a `CLIPath` n
104104
- `ResumeSessionWithOptions(sessionID string, config *ResumeSessionConfig) (*Session, error)` - Resume with additional configuration
105105
- `ListSessions(filter *SessionListFilter) ([]SessionMetadata, error)` - List sessions (with optional filter)
106106
- `DeleteSession(sessionID string) error` - Delete a session permanently
107+
- `GetLastSessionID(ctx context.Context) (*string, error)` - Get the ID of the most recently updated session
107108
- `GetState() ConnectionState` - Get connection state
108109
- `Ping(message string) (*PingResponse, error)` - Ping the server
109110
- `GetForegroundSessionID(ctx context.Context) (*string, error)` - Get the session ID currently displayed in TUI (TUI+server mode only)

go/client.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,40 @@ func (c *Client) DeleteSession(ctx context.Context, sessionID string) error {
726726
return nil
727727
}
728728

729+
// GetLastSessionID returns the ID of the most recently updated session.
730+
//
731+
// This is useful for resuming the last conversation when the session ID
732+
// was not stored. Returns nil if no sessions exist.
733+
//
734+
// Example:
735+
//
736+
// lastID, err := client.GetLastSessionID(context.Background())
737+
// if err != nil {
738+
// log.Fatal(err)
739+
// }
740+
// if lastID != nil {
741+
// session, err := client.ResumeSession(context.Background(), *lastID, &copilot.ResumeSessionConfig{
742+
// OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
743+
// })
744+
// }
745+
func (c *Client) GetLastSessionID(ctx context.Context) (*string, error) {
746+
if err := c.ensureConnected(); err != nil {
747+
return nil, err
748+
}
749+
750+
result, err := c.client.Request("session.getLastId", getLastSessionIDRequest{})
751+
if err != nil {
752+
return nil, err
753+
}
754+
755+
var response getLastSessionIDResponse
756+
if err := json.Unmarshal(result, &response); err != nil {
757+
return nil, fmt.Errorf("failed to unmarshal getLastId response: %w", err)
758+
}
759+
760+
return response.SessionID, nil
761+
}
762+
729763
// GetForegroundSessionID returns the ID of the session currently displayed in the TUI.
730764
//
731765
// This is only available when connecting to a server running in TUI+server mode

go/internal/e2e/session_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,40 @@ func TestSession(t *testing.T) {
828828
t.Error("Expected error when resuming deleted session")
829829
}
830830
})
831+
t.Run("should get last session id", func(t *testing.T) {
832+
ctx.ConfigureForTest(t)
833+
834+
// Create a session and send a message to persist it
835+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{OnPermissionRequest: copilot.PermissionHandler.ApproveAll})
836+
if err != nil {
837+
t.Fatalf("Failed to create session: %v", err)
838+
}
839+
840+
_, err = session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "Say hello"})
841+
if err != nil {
842+
t.Fatalf("Failed to send message: %v", err)
843+
}
844+
845+
// Small delay to ensure session data is flushed to disk
846+
time.Sleep(500 * time.Millisecond)
847+
848+
lastSessionID, err := client.GetLastSessionID(t.Context())
849+
if err != nil {
850+
t.Fatalf("Failed to get last session ID: %v", err)
851+
}
852+
853+
if lastSessionID == nil {
854+
t.Fatal("Expected last session ID to be non-nil")
855+
}
856+
857+
if *lastSessionID != session.SessionID {
858+
t.Errorf("Expected last session ID to be %s, got %s", session.SessionID, *lastSessionID)
859+
}
860+
861+
if err := session.Destroy(); err != nil {
862+
t.Fatalf("Failed to destroy session: %v", err)
863+
}
864+
})
831865
}
832866

833867
func getSystemMessage(exchange testharness.ParsedHttpExchange) string {

go/types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,6 +749,14 @@ type deleteSessionResponse struct {
749749
Error *string `json:"error,omitempty"`
750750
}
751751

752+
// getLastSessionIDRequest is the request for session.getLastId
753+
type getLastSessionIDRequest struct{}
754+
755+
// getLastSessionIDResponse is the response from session.getLastId
756+
type getLastSessionIDResponse struct {
757+
SessionID *string `json:"sessionId,omitempty"`
758+
}
759+
752760
// getForegroundSessionRequest is the request for session.getForeground
753761
type getForegroundSessionRequest struct{}
754762

python/copilot/client.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,6 +961,30 @@ async def delete_session(self, session_id: str) -> None:
961961
if session_id in self._sessions:
962962
del self._sessions[session_id]
963963

964+
async def get_last_session_id(self) -> str | None:
965+
"""
966+
Get the ID of the most recently updated session.
967+
968+
This is useful for resuming the last conversation when the session ID
969+
was not stored.
970+
971+
Returns:
972+
The session ID, or None if no sessions exist.
973+
974+
Raises:
975+
RuntimeError: If the client is not connected.
976+
977+
Example:
978+
>>> last_id = await client.get_last_session_id()
979+
>>> if last_id:
980+
... session = await client.resume_session(last_id, {"on_permission_request": PermissionHandler.approve_all})
981+
"""
982+
if not self._client:
983+
raise RuntimeError("Client not connected")
984+
985+
response = await self._client.request("session.getLastId", {})
986+
return response.get("sessionId")
987+
964988
async def get_foreground_session_id(self) -> str | None:
965989
"""
966990
Get the ID of the session currently displayed in the TUI.

python/e2e/test_session.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,23 @@ async def test_should_delete_session(self, ctx: E2ETestContext):
303303
session_id, {"on_permission_request": PermissionHandler.approve_all}
304304
)
305305

306+
async def test_should_get_last_session_id(self, ctx: E2ETestContext):
307+
import asyncio
308+
309+
# Create a session and send a message to persist it
310+
session = await ctx.client.create_session(
311+
{"on_permission_request": PermissionHandler.approve_all}
312+
)
313+
await session.send_and_wait({"prompt": "Say hello"})
314+
315+
# Small delay to ensure session data is flushed to disk
316+
await asyncio.sleep(0.5)
317+
318+
last_session_id = await ctx.client.get_last_session_id()
319+
assert last_session_id == session.session_id
320+
321+
await session.destroy()
322+
306323
async def test_should_create_session_with_custom_tool(self, ctx: E2ETestContext):
307324
# This test uses the low-level Tool() API to show that Pydantic is optional
308325
def get_secret_number_handler(invocation):
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
models:
2+
- claude-sonnet-4.5
3+
conversations:
4+
- messages:
5+
- role: system
6+
content: ${system}
7+
- role: user
8+
content: Say hello
9+
- role: assistant
10+
content: Hello! I'm GitHub Copilot CLI, ready to help with your software engineering tasks.

0 commit comments

Comments
 (0)