Skip to content

Commit bc82988

Browse files
Route Python hooks.invoke through generated client-global handler
CLI 1.0.71 promoted hooks.invoke to a client-global RPC method with a generated HooksHandler interface. The handwritten SDK still registered its own hooks.invoke handler, which only avoided colliding with the generated one because global handlers were skipped when no LLM/telemetry adapter was set. Make the wiring intentional: add _HooksAdapter implementing the generated HooksHandler protocol (routing HookInvokeRequest.sessionId to the matching session's dispatcher), always register the client-global handlers with the hooks adapter, and remove the redundant handwritten hooks.invoke registrations and dead client-level handler. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ae226b7f-caf9-46f3-b8c5-b9a21c5d7951
1 parent 8f39116 commit bc82988

2 files changed

Lines changed: 50 additions & 35 deletions

File tree

python/copilot/client.py

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
RemoteSessionMode,
7474
ServerRpc,
7575
_ConnectResult,
76+
_HookInvokeRequest,
77+
_HookInvokeResponse,
7678
from_datetime,
7779
register_client_global_api_handlers,
7880
register_client_session_api_handlers,
@@ -479,6 +481,25 @@ async def event(self, params: GitHubTelemetryNotification) -> None:
479481
logger.warning("Error handling gitHubTelemetry.event notification", exc_info=True)
480482

481483

484+
class _HooksAdapter:
485+
"""Adapts session-scoped hook dispatch to the generated ``HooksHandler`` protocol.
486+
487+
``hooks.invoke`` is a client-global RPC method whose payload carries a
488+
``sessionId``. This adapter routes each invocation to the matching session's
489+
registered hook handlers.
490+
"""
491+
492+
def __init__(self, get_session: Callable[[str], CopilotSession | None]) -> None:
493+
self._get_session = get_session
494+
495+
async def invoke(self, params: _HookInvokeRequest) -> _HookInvokeResponse:
496+
session = self._get_session(params.session_id)
497+
if session is None:
498+
raise ValueError(f"unknown session {params.session_id}")
499+
output = await session._handle_hooks_invoke(params.hook_type.value, params.input)
500+
return _HookInvokeResponse(output=output)
501+
502+
482503
@dataclass
483504
class _CopilotClientOptions:
484505
"""Internal configuration carrier used by :class:`CopilotClient`.
@@ -4072,7 +4093,6 @@ def handle_notification(method: str, params: dict):
40724093
self._client.set_request_handler(
40734094
"autoModeSwitch.request", self._handle_auto_mode_switch_request
40744095
)
4075-
self._client.set_request_handler("hooks.invoke", self._handle_hooks_invoke)
40764096
self._client.set_request_handler(
40774097
"systemMessage.transform", self._handle_system_message_transform
40784098
)
@@ -4192,7 +4212,6 @@ def handle_notification(method: str, params: dict):
41924212
self._client.set_request_handler(
41934213
"autoModeSwitch.request", self._handle_auto_mode_switch_request
41944214
)
4195-
self._client.set_request_handler("hooks.invoke", self._handle_hooks_invoke)
41964215
self._client.set_request_handler(
41974216
"systemMessage.transform", self._handle_system_message_transform
41984217
)
@@ -4282,16 +4301,19 @@ def _register_client_global_handlers(self) -> None:
42824301
github_telemetry_adapter = None
42834302
if self._on_github_telemetry is not None:
42844303
github_telemetry_adapter = _GitHubTelemetryAdapter(self._on_github_telemetry)
4285-
if llm_inference_adapter is None and github_telemetry_adapter is None:
4286-
return
42874304
register_client_global_api_handlers(
42884305
self._client,
42894306
ClientGlobalApiHandlers(
4307+
hooks=_HooksAdapter(self._get_session),
42904308
llm_inference=llm_inference_adapter,
42914309
git_hub_telemetry=github_telemetry_adapter,
42924310
),
42934311
)
42944312

4313+
def _get_session(self, session_id: str) -> CopilotSession | None:
4314+
with self._sessions_lock:
4315+
return self._sessions.get(session_id)
4316+
42954317
async def _set_llm_inference_provider(self) -> None:
42964318
if self._request_handler is None or self._rpc is None:
42974319
return
@@ -4364,34 +4386,6 @@ async def _handle_auto_mode_switch_request(self, params: dict) -> dict:
43644386
response = await session._handle_auto_mode_switch_request(params)
43654387
return {"response": response}
43664388

4367-
async def _handle_hooks_invoke(self, params: dict) -> dict:
4368-
"""
4369-
Handle a hooks invocation from the CLI server.
4370-
4371-
Args:
4372-
params: The hooks invocation parameters from the server.
4373-
4374-
Returns:
4375-
A dict containing the hook output.
4376-
4377-
Raises:
4378-
ValueError: If the request payload is invalid.
4379-
"""
4380-
session_id = params.get("sessionId")
4381-
hook_type = params.get("hookType")
4382-
input_data = params.get("input")
4383-
4384-
if not session_id or not hook_type:
4385-
raise ValueError("invalid hooks invoke payload")
4386-
4387-
with self._sessions_lock:
4388-
session = self._sessions.get(session_id)
4389-
if not session:
4390-
raise ValueError(f"unknown session {session_id}")
4391-
4392-
output = await session._handle_hooks_invoke(hook_type, input_data)
4393-
return {"output": output}
4394-
43954389
async def _handle_system_message_transform(self, params: dict) -> dict:
43964390
"""Handle a systemMessage.transform request from the CLI server."""
43974391
session_id = params.get("sessionId")

python/test_client.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2616,12 +2616,33 @@ async def on_telemetry(notification):
26162616
await client.force_stop()
26172617

26182618
@pytest.mark.asyncio
2619-
async def test_event_handler_not_registered_without_option(self):
2619+
async def test_event_not_forwarded_without_option(self):
2620+
# Client-global handlers are always registered (so that hooks.invoke works),
2621+
# but without the on_github_telemetry option the telemetry adapter is inert:
2622+
# incoming events must not be forwarded to any callback.
26202623
client = CopilotClient(connection=RuntimeConnection.for_stdio(path=CLI_PATH))
26212624
await client.start()
26222625

26232626
try:
2624-
assert "gitHubTelemetry.event" not in client._client.notification_method_handlers
2625-
assert "gitHubTelemetry.event" not in client._client.request_handlers
2627+
assert client._on_github_telemetry is None
2628+
2629+
# Dispatching a telemetry event is a harmless no-op when not opted in.
2630+
client._client._handle_message(
2631+
{
2632+
"jsonrpc": "2.0",
2633+
"method": "gitHubTelemetry.event",
2634+
"params": {
2635+
"sessionId": "sess-no-telemetry",
2636+
"restricted": False,
2637+
"event": {
2638+
"kind": "tool_call_executed",
2639+
"metrics": {"duration_ms": 1.0},
2640+
"properties": {"tool": "shell"},
2641+
"session_id": "sess-no-telemetry",
2642+
},
2643+
},
2644+
}
2645+
)
2646+
await asyncio.sleep(0)
26262647
finally:
26272648
await client.force_stop()

0 commit comments

Comments
 (0)