From 4e8bbe80b033b6c32c95b564d16eaa9c1a2d04f6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:32:25 +0000 Subject: [PATCH 1/5] Initial plan From 66bd11ac005c41c6095c9bee4b383ea550209e65 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:36:23 +0000 Subject: [PATCH 2/5] Add list_sessions method to Python SDK client Co-authored-by: friggeri <106686+friggeri@users.noreply.github.com> --- python/copilot/__init__.py | 2 ++ python/copilot/client.py | 25 +++++++++++++++++++++++++ python/copilot/types.py | 10 ++++++++++ python/e2e/test_session.py | 25 +++++++++++++++++++++++++ 4 files changed, 62 insertions(+) diff --git a/python/copilot/__init__.py b/python/copilot/__init__.py index 47a4ab6d..f5961472 100644 --- a/python/copilot/__init__.py +++ b/python/copilot/__init__.py @@ -28,6 +28,7 @@ ResumeSessionConfig, SessionConfig, SessionEvent, + SessionMetadata, Tool, ToolHandler, ToolInvocation, @@ -59,6 +60,7 @@ "ResumeSessionConfig", "SessionConfig", "SessionEvent", + "SessionMetadata", "Tool", "ToolHandler", "ToolInvocation", diff --git a/python/copilot/client.py b/python/copilot/client.py index 030ee4f2..c69ce4b7 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -633,6 +633,31 @@ async def list_models(self) -> List["ModelInfo"]: response = await self._client.request("models.list", {}) return response.get("models", []) + async def list_sessions(self) -> List[Dict[str, Any]]: + """ + List all available sessions known to the server. + + Returns metadata about each session including ID, timestamps, and summary. + + Returns: + A list of session metadata dictionaries with keys: sessionId (str), + startTime (str), modifiedTime (str), summary (str, optional), + and isRemote (bool). + + Raises: + RuntimeError: If the client is not connected. + + Example: + >>> sessions = await client.list_sessions() + >>> for session in sessions: + ... print(f"Session: {session['sessionId']}") + """ + if not self._client: + raise RuntimeError("Client not connected") + + response = await self._client.request("session.list", {}) + return response.get("sessions", []) + async def _verify_protocol_version(self) -> None: """Verify that the server's protocol version matches the SDK's expected version.""" expected_version = get_sdk_protocol_version() diff --git a/python/copilot/types.py b/python/copilot/types.py index 6a4d0b8d..ddf960f7 100644 --- a/python/copilot/types.py +++ b/python/copilot/types.py @@ -359,3 +359,13 @@ class GetModelsResponse(TypedDict): """Response from models.list""" models: List[ModelInfo] + + +class SessionMetadata(TypedDict): + """Metadata about a session""" + + sessionId: str # Session identifier + startTime: str # ISO 8601 timestamp when session was created + modifiedTime: str # ISO 8601 timestamp when session was last modified + summary: NotRequired[str] # Optional summary of the session + isRemote: bool # Whether the session is remote diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index e4dc7db9..fff91ee4 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -180,6 +180,31 @@ async def test_should_throw_error_resuming_nonexistent_session(self, ctx: E2ETes with pytest.raises(Exception): await ctx.client.resume_session("non-existent-session-id") + async def test_should_list_sessions(self, ctx: E2ETestContext): + # Create a couple of sessions + session1 = await ctx.client.create_session() + session2 = await ctx.client.create_session() + + # List sessions and verify they're included + sessions = await ctx.client.list_sessions() + assert isinstance(sessions, list) + + session_ids = [s["sessionId"] for s in sessions] + assert session1.session_id in session_ids + assert session2.session_id in session_ids + + # Verify session metadata structure + for session_data in sessions: + assert "sessionId" in session_data + assert "startTime" in session_data + assert "modifiedTime" in session_data + assert "isRemote" in session_data + # summary is optional + assert isinstance(session_data["sessionId"], str) + assert isinstance(session_data["startTime"], str) + assert isinstance(session_data["modifiedTime"], str) + assert isinstance(session_data["isRemote"], bool) + async def test_should_create_session_with_custom_tool(self, ctx: E2ETestContext): # This test uses the low-level Tool() API to show that Pydantic is optional def get_secret_number_handler(invocation): From c8cead4c14fa2b0f542c5075b4f7f8aaff8125d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:38:52 +0000 Subject: [PATCH 3/5] Improve type annotations for list_sessions method Co-authored-by: friggeri <106686+friggeri@users.noreply.github.com> --- python/copilot/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/python/copilot/client.py b/python/copilot/client.py index c69ce4b7..c81c4e3e 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -33,6 +33,7 @@ ModelInfo, ResumeSessionConfig, SessionConfig, + SessionMetadata, ToolHandler, ToolInvocation, ToolResult, @@ -633,7 +634,7 @@ async def list_models(self) -> List["ModelInfo"]: response = await self._client.request("models.list", {}) return response.get("models", []) - async def list_sessions(self) -> List[Dict[str, Any]]: + async def list_sessions(self) -> List["SessionMetadata"]: """ List all available sessions known to the server. From 26c7e1806b75e73e65d9061bc35a9fab73347d21 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:45:08 +0000 Subject: [PATCH 4/5] Add delete_session method to Python SDK client Co-authored-by: friggeri <106686+friggeri@users.noreply.github.com> --- python/copilot/client.py | 32 ++++++++++++++++++++++++++++++++ python/e2e/test_session.py | 22 ++++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/python/copilot/client.py b/python/copilot/client.py index c81c4e3e..288778a3 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -659,6 +659,38 @@ async def list_sessions(self) -> List["SessionMetadata"]: response = await self._client.request("session.list", {}) return response.get("sessions", []) + async def delete_session(self, session_id: str) -> None: + """ + Delete a session permanently. + + This permanently removes the session and all its conversation history. + The session cannot be resumed after deletion. + + Args: + session_id: The ID of the session to delete. + + Raises: + RuntimeError: If the client is not connected. + Exception: If the session does not exist or deletion fails. + + Example: + >>> await client.delete_session("session-123") + """ + if not self._client: + raise RuntimeError("Client not connected") + + response = await self._client.request("session.delete", {"sessionId": session_id}) + + success = response.get("success", False) + if not success: + error = response.get("error", "Unknown error") + raise Exception(f"Failed to delete session {session_id}: {error}") + + # Remove from local sessions map if present + with self._sessions_lock: + if session_id in self._sessions: + del self._sessions[session_id] + async def _verify_protocol_version(self) -> None: """Verify that the server's protocol version matches the SDK's expected version.""" expected_version = get_sdk_protocol_version() diff --git a/python/e2e/test_session.py b/python/e2e/test_session.py index fff91ee4..cc6b6300 100644 --- a/python/e2e/test_session.py +++ b/python/e2e/test_session.py @@ -205,6 +205,28 @@ async def test_should_list_sessions(self, ctx: E2ETestContext): assert isinstance(session_data["modifiedTime"], str) assert isinstance(session_data["isRemote"], bool) + async def test_should_delete_session(self, ctx: E2ETestContext): + # Create a session + session = await ctx.client.create_session() + session_id = session.session_id + + # Verify session exists in the list + sessions = await ctx.client.list_sessions() + session_ids = [s["sessionId"] for s in sessions] + assert session_id in session_ids + + # Delete the session + await ctx.client.delete_session(session_id) + + # Verify session no longer exists in the list + sessions_after = await ctx.client.list_sessions() + session_ids_after = [s["sessionId"] for s in sessions_after] + assert session_id not in session_ids_after + + # Verify we cannot resume the deleted session + with pytest.raises(Exception): + await ctx.client.resume_session(session_id) + async def test_should_create_session_with_custom_tool(self, ctx: E2ETestContext): # This test uses the low-level Tool() API to show that Pydantic is optional def get_secret_number_handler(invocation): From d08174ce5c17abda4a3cef70ce280f787b769e27 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 17:46:59 +0000 Subject: [PATCH 5/5] Use RuntimeError for consistent exception handling in delete_session Co-authored-by: friggeri <106686+friggeri@users.noreply.github.com> --- python/copilot/client.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/python/copilot/client.py b/python/copilot/client.py index 288778a3..efb2b9d5 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -670,8 +670,7 @@ async def delete_session(self, session_id: str) -> None: session_id: The ID of the session to delete. Raises: - RuntimeError: If the client is not connected. - Exception: If the session does not exist or deletion fails. + RuntimeError: If the client is not connected or deletion fails. Example: >>> await client.delete_session("session-123") @@ -684,7 +683,7 @@ async def delete_session(self, session_id: str) -> None: success = response.get("success", False) if not success: error = response.get("error", "Unknown error") - raise Exception(f"Failed to delete session {session_id}: {error}") + raise RuntimeError(f"Failed to delete session {session_id}: {error}") # Remove from local sessions map if present with self._sessions_lock: