|
13 | 13 | """ |
14 | 14 |
|
15 | 15 | import asyncio |
16 | | -import inspect |
17 | 16 | import os |
18 | 17 | import re |
19 | 18 | import subprocess |
20 | 19 | import sys |
21 | 20 | import threading |
22 | 21 | from collections.abc import Callable |
23 | | -from dataclasses import asdict, is_dataclass |
24 | 22 | from pathlib import Path |
25 | 23 | from typing import Any, cast |
26 | 24 |
|
|
46 | 44 | SessionListFilter, |
47 | 45 | SessionMetadata, |
48 | 46 | StopError, |
49 | | - ToolHandler, |
50 | | - ToolInvocation, |
51 | | - ToolResult, |
52 | 47 | ) |
53 | 48 |
|
54 | 49 |
|
@@ -219,6 +214,16 @@ def rpc(self) -> ServerRpc: |
219 | 214 | raise RuntimeError("Client is not connected. Call start() first.") |
220 | 215 | return self._rpc |
221 | 216 |
|
| 217 | + @property |
| 218 | + def actual_port(self) -> int | None: |
| 219 | + """The actual TCP port the CLI server is listening on, if using TCP transport. |
| 220 | +
|
| 221 | + Useful for multi-client scenarios where a second client needs to connect |
| 222 | + to the same server. Only available after :meth:`start` completes and |
| 223 | + only when not using stdio transport. |
| 224 | + """ |
| 225 | + return self._actual_port |
| 226 | + |
222 | 227 | def _parse_cli_url(self, url: str) -> tuple[str, int]: |
223 | 228 | """ |
224 | 229 | Parse CLI URL into host and port. |
@@ -1354,8 +1359,10 @@ def handle_notification(method: str, params: dict): |
1354 | 1359 | self._dispatch_lifecycle_event(lifecycle_event) |
1355 | 1360 |
|
1356 | 1361 | self._client.set_notification_handler(handle_notification) |
1357 | | - self._client.set_request_handler("tool.call", self._handle_tool_call_request) |
1358 | | - self._client.set_request_handler("permission.request", self._handle_permission_request) |
| 1362 | + # Protocol v3: tool.call and permission.request RPC handlers removed. |
| 1363 | + # Tool calls and permission requests are now broadcast as session events |
| 1364 | + # (external_tool.requested, permission.requested) and handled in |
| 1365 | + # Session._handle_broadcast_event. |
1359 | 1366 | self._client.set_request_handler("userInput.request", self._handle_user_input_request) |
1360 | 1367 | self._client.set_request_handler("hooks.invoke", self._handle_hooks_invoke) |
1361 | 1368 |
|
@@ -1435,50 +1442,15 @@ def handle_notification(method: str, params: dict): |
1435 | 1442 | self._dispatch_lifecycle_event(lifecycle_event) |
1436 | 1443 |
|
1437 | 1444 | self._client.set_notification_handler(handle_notification) |
1438 | | - self._client.set_request_handler("tool.call", self._handle_tool_call_request) |
1439 | | - self._client.set_request_handler("permission.request", self._handle_permission_request) |
| 1445 | + # Protocol v3: tool.call and permission.request RPC handlers removed. |
| 1446 | + # See _connect_via_stdio for details. |
1440 | 1447 | self._client.set_request_handler("userInput.request", self._handle_user_input_request) |
1441 | 1448 | self._client.set_request_handler("hooks.invoke", self._handle_hooks_invoke) |
1442 | 1449 |
|
1443 | 1450 | # Start listening for messages |
1444 | 1451 | loop = asyncio.get_running_loop() |
1445 | 1452 | self._client.start(loop) |
1446 | 1453 |
|
1447 | | - async def _handle_permission_request(self, params: dict) -> dict: |
1448 | | - """ |
1449 | | - Handle a permission request from the CLI server. |
1450 | | -
|
1451 | | - Args: |
1452 | | - params: The permission request parameters from the server. |
1453 | | -
|
1454 | | - Returns: |
1455 | | - A dict containing the permission decision result. |
1456 | | -
|
1457 | | - Raises: |
1458 | | - ValueError: If the request payload is invalid. |
1459 | | - """ |
1460 | | - session_id = params.get("sessionId") |
1461 | | - permission_request = params.get("permissionRequest") |
1462 | | - |
1463 | | - if not session_id or not permission_request: |
1464 | | - raise ValueError("invalid permission request payload") |
1465 | | - |
1466 | | - with self._sessions_lock: |
1467 | | - session = self._sessions.get(session_id) |
1468 | | - if not session: |
1469 | | - raise ValueError(f"unknown session {session_id}") |
1470 | | - |
1471 | | - try: |
1472 | | - result = await session._handle_permission_request(permission_request) |
1473 | | - return {"result": result} |
1474 | | - except Exception: # pylint: disable=broad-except |
1475 | | - # If permission handler fails, deny the permission |
1476 | | - return { |
1477 | | - "result": { |
1478 | | - "kind": "denied-no-approval-rule-and-could-not-request-from-user", |
1479 | | - } |
1480 | | - } |
1481 | | - |
1482 | 1454 | async def _handle_user_input_request(self, params: dict) -> dict: |
1483 | 1455 | """ |
1484 | 1456 | Handle a user input request from the CLI server. |
@@ -1533,129 +1505,3 @@ async def _handle_hooks_invoke(self, params: dict) -> dict: |
1533 | 1505 |
|
1534 | 1506 | output = await session._handle_hooks_invoke(hook_type, input_data) |
1535 | 1507 | return {"output": output} |
1536 | | - |
1537 | | - async def _handle_tool_call_request(self, params: dict) -> dict: |
1538 | | - """ |
1539 | | - Handle a tool call request from the CLI server. |
1540 | | -
|
1541 | | - Args: |
1542 | | - params: The tool call parameters from the server. |
1543 | | -
|
1544 | | - Returns: |
1545 | | - A dict containing the tool execution result. |
1546 | | -
|
1547 | | - Raises: |
1548 | | - ValueError: If the request payload is invalid or session is unknown. |
1549 | | - """ |
1550 | | - session_id = params.get("sessionId") |
1551 | | - tool_call_id = params.get("toolCallId") |
1552 | | - tool_name = params.get("toolName") |
1553 | | - |
1554 | | - if not session_id or not tool_call_id or not tool_name: |
1555 | | - raise ValueError("invalid tool call payload") |
1556 | | - |
1557 | | - with self._sessions_lock: |
1558 | | - session = self._sessions.get(session_id) |
1559 | | - if not session: |
1560 | | - raise ValueError(f"unknown session {session_id}") |
1561 | | - |
1562 | | - handler = session._get_tool_handler(tool_name) |
1563 | | - if not handler: |
1564 | | - return {"result": self._build_unsupported_tool_result(tool_name)} |
1565 | | - |
1566 | | - arguments = params.get("arguments") |
1567 | | - result = await self._execute_tool_call( |
1568 | | - session_id, |
1569 | | - tool_call_id, |
1570 | | - tool_name, |
1571 | | - arguments, |
1572 | | - handler, |
1573 | | - ) |
1574 | | - |
1575 | | - return {"result": result} |
1576 | | - |
1577 | | - async def _execute_tool_call( |
1578 | | - self, |
1579 | | - session_id: str, |
1580 | | - tool_call_id: str, |
1581 | | - tool_name: str, |
1582 | | - arguments: Any, |
1583 | | - handler: ToolHandler, |
1584 | | - ) -> ToolResult: |
1585 | | - """ |
1586 | | - Execute a tool call with the given handler. |
1587 | | -
|
1588 | | - Args: |
1589 | | - session_id: The session ID making the tool call. |
1590 | | - tool_call_id: The unique ID for this tool call. |
1591 | | - tool_name: The name of the tool being called. |
1592 | | - arguments: The arguments to pass to the tool handler. |
1593 | | - handler: The tool handler function to execute. |
1594 | | -
|
1595 | | - Returns: |
1596 | | - A ToolResult containing the execution result or error. |
1597 | | - """ |
1598 | | - invocation: ToolInvocation = { |
1599 | | - "session_id": session_id, |
1600 | | - "tool_call_id": tool_call_id, |
1601 | | - "tool_name": tool_name, |
1602 | | - "arguments": arguments, |
1603 | | - } |
1604 | | - |
1605 | | - try: |
1606 | | - result = handler(invocation) |
1607 | | - if inspect.isawaitable(result): |
1608 | | - result = await result |
1609 | | - except Exception as exc: # pylint: disable=broad-except |
1610 | | - # Don't expose detailed error information to the LLM for security reasons. |
1611 | | - # The actual error is stored in the 'error' field for debugging. |
1612 | | - result = ToolResult( |
1613 | | - textResultForLlm="Invoking this tool produced an error. " |
1614 | | - "Detailed information is not available.", |
1615 | | - resultType="failure", |
1616 | | - error=str(exc), |
1617 | | - toolTelemetry={}, |
1618 | | - ) |
1619 | | - |
1620 | | - if result is None: |
1621 | | - result = ToolResult( |
1622 | | - textResultForLlm="Tool returned no result.", |
1623 | | - resultType="failure", |
1624 | | - error="tool returned no result", |
1625 | | - toolTelemetry={}, |
1626 | | - ) |
1627 | | - |
1628 | | - return self._normalize_tool_result(cast(ToolResult, result)) |
1629 | | - |
1630 | | - def _normalize_tool_result(self, result: ToolResult) -> ToolResult: |
1631 | | - """ |
1632 | | - Normalize a tool result for transmission. |
1633 | | -
|
1634 | | - Converts dataclass instances to dictionaries for JSON serialization. |
1635 | | -
|
1636 | | - Args: |
1637 | | - result: The tool result to normalize. |
1638 | | -
|
1639 | | - Returns: |
1640 | | - The normalized tool result. |
1641 | | - """ |
1642 | | - if is_dataclass(result) and not isinstance(result, type): |
1643 | | - return asdict(result) # type: ignore[arg-type] |
1644 | | - return result |
1645 | | - |
1646 | | - def _build_unsupported_tool_result(self, tool_name: str) -> ToolResult: |
1647 | | - """ |
1648 | | - Build a failure result for an unsupported tool. |
1649 | | -
|
1650 | | - Args: |
1651 | | - tool_name: The name of the unsupported tool. |
1652 | | -
|
1653 | | - Returns: |
1654 | | - A ToolResult indicating the tool is not supported. |
1655 | | - """ |
1656 | | - return ToolResult( |
1657 | | - textResultForLlm=f"Tool '{tool_name}' is not supported.", |
1658 | | - resultType="failure", |
1659 | | - error=f"tool '{tool_name}' not supported", |
1660 | | - toolTelemetry={}, |
1661 | | - ) |
0 commit comments