From 4a37c88222d5198f65cbc0efd7f216e827ae8166 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Wed, 25 Feb 2026 16:12:51 -0800 Subject: [PATCH 1/4] feat(python): add overloads for `CopilotClient.on()` --- python/copilot/client.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/python/copilot/client.py b/python/copilot/client.py index 774569afb..9cf6d5d3e 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -21,7 +21,7 @@ import threading from dataclasses import asdict, is_dataclass from pathlib import Path -from typing import Any, Callable, Optional, cast +from typing import Any, Callable, Optional, cast, overload from .generated.rpc import ServerRpc from .generated.session_events import session_event_from_dict @@ -1006,9 +1006,16 @@ async def set_foreground_session_id(self, session_id: str) -> None: error = response.get("error", "Unknown error") raise RuntimeError(f"Failed to set foreground session: {error}") + @overload + def on(self, handler: SessionLifecycleHandler, /) -> ...: ... + + @overload + def on(self, event_type: SessionLifecycleEventType, /, handler: ...) -> ...: ... + def on( self, event_type_or_handler: SessionLifecycleEventType | SessionLifecycleHandler, + /, handler: Optional[SessionLifecycleHandler] = None, ) -> Callable[[], None]: """ From b843ab65e4c5bdb4e1581c0b121d13c7cb5ec87d Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 26 Feb 2026 11:30:27 -0800 Subject: [PATCH 2/4] Carry forward types in overloads --- python/copilot/client.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/python/copilot/client.py b/python/copilot/client.py index 9cf6d5d3e..2f855dbca 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -51,6 +51,9 @@ ) +UnsubscribeHandler = Callable[[], None] + + def _get_bundled_cli_path() -> Optional[str]: """Get the path to the bundled CLI binary, if available.""" # The binary is bundled in copilot/bin/ within the package @@ -1007,17 +1010,19 @@ async def set_foreground_session_id(self, session_id: str) -> None: raise RuntimeError(f"Failed to set foreground session: {error}") @overload - def on(self, handler: SessionLifecycleHandler, /) -> ...: ... + def on(self, handler: SessionLifecycleHandler, /) -> UnsubscribeHandler: ... @overload - def on(self, event_type: SessionLifecycleEventType, /, handler: ...) -> ...: ... + def on( + self, event_type: SessionLifecycleEventType, /, handler: SessionLifecycleHandler + ) -> UnsubscribeHandler: ... def on( self, event_type_or_handler: SessionLifecycleEventType | SessionLifecycleHandler, /, handler: Optional[SessionLifecycleHandler] = None, - ) -> Callable[[], None]: + ) -> UnsubscribeHandler: """ Subscribe to session lifecycle events. From ef0c61606985dee8e2a29d55dacc890530576c7a Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 26 Feb 2026 11:35:30 -0800 Subject: [PATCH 3/4] Rename UnsubscribeHandler to HandlerUnsubcribe in type annotations --- python/copilot/client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/copilot/client.py b/python/copilot/client.py index 2f855dbca..e918cf911 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -51,7 +51,7 @@ ) -UnsubscribeHandler = Callable[[], None] +HandlerUnsubcribe = Callable[[], None] def _get_bundled_cli_path() -> Optional[str]: @@ -1010,19 +1010,19 @@ async def set_foreground_session_id(self, session_id: str) -> None: raise RuntimeError(f"Failed to set foreground session: {error}") @overload - def on(self, handler: SessionLifecycleHandler, /) -> UnsubscribeHandler: ... + def on(self, handler: SessionLifecycleHandler, /) -> HandlerUnsubcribe: ... @overload def on( self, event_type: SessionLifecycleEventType, /, handler: SessionLifecycleHandler - ) -> UnsubscribeHandler: ... + ) -> HandlerUnsubcribe: ... def on( self, event_type_or_handler: SessionLifecycleEventType | SessionLifecycleHandler, /, handler: Optional[SessionLifecycleHandler] = None, - ) -> UnsubscribeHandler: + ) -> HandlerUnsubcribe: """ Subscribe to session lifecycle events. From 65a892432b3baceace3755cc5c71216a52404df0 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Thu, 26 Feb 2026 13:09:20 -0800 Subject: [PATCH 4/4] Fix type checking --- python/copilot/client.py | 8 ++++---- python/copilot/session.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/python/copilot/client.py b/python/copilot/client.py index e918cf911..a5a7ef2e0 100644 --- a/python/copilot/client.py +++ b/python/copilot/client.py @@ -50,7 +50,6 @@ ToolResult, ) - HandlerUnsubcribe = Callable[[], None] @@ -1579,9 +1578,10 @@ async def _execute_tool_call( } try: - result = handler(invocation) - if inspect.isawaitable(result): - result = await result + raw_result = handler(invocation) + if inspect.isawaitable(raw_result): + raw_result = await raw_result + result: ToolResult = cast(ToolResult, raw_result) except Exception as exc: # pylint: disable=broad-except # Don't expose detailed error information to the LLM for security reasons. # The actual error is stored in the 'error' field for debugging. diff --git a/python/copilot/session.py b/python/copilot/session.py index af02a312d..658d2902a 100644 --- a/python/copilot/session.py +++ b/python/copilot/session.py @@ -8,7 +8,7 @@ import asyncio import inspect import threading -from typing import Any, Callable, Optional +from typing import Any, Callable, Optional, cast from .generated.rpc import SessionRpc from .generated.session_events import SessionEvent, SessionEventType, session_event_from_dict @@ -336,7 +336,7 @@ async def _handle_permission_request( result = handler(request, {"session_id": self.session_id}) if inspect.isawaitable(result): result = await result - return result + return cast(PermissionRequestResult, result) except Exception: # pylint: disable=broad-except # Handler failed, deny permission return {"kind": "denied-no-approval-rule-and-could-not-request-from-user"} @@ -388,7 +388,7 @@ async def _handle_user_input_request(self, request: dict) -> UserInputResponse: ) if inspect.isawaitable(result): result = await result - return result + return cast(UserInputResponse, result) except Exception: raise