|
5 | 5 | import logging |
6 | 6 | from asyncio import StreamReader, StreamWriter |
7 | 7 | from typing import Any, Awaitable, Callable, Coroutine, Optional, Sequence |
| 8 | +from uuid import uuid4 |
8 | 9 |
|
9 | 10 | from .bash_executor import get_bash_executor, ToolExecutionResult |
10 | 11 | from .sandbox import ExecutionContext |
@@ -379,6 +380,12 @@ async def handler(payload: dict[str, Any]) -> None: |
379 | 380 | "payload_keys": list(payload.keys()) |
380 | 381 | }) |
381 | 382 |
|
| 383 | + # Handle agent-initiated requests (e.g., fs/read_text_file) |
| 384 | + if payload.get("id") is not None and payload.get("method"): |
| 385 | + handled = await self._handle_agent_request(payload, session_id) |
| 386 | + if handled: |
| 387 | + return |
| 388 | + |
382 | 389 | if payload.get("method") == "session/update": |
383 | 390 | params = payload.get("params", {}) |
384 | 391 | update_data = params.get("update", {}) |
@@ -580,6 +587,131 @@ def stderr(self) -> str: |
580 | 587 | """Return aggregated stderr output.""" |
581 | 588 | return "\n".join(self._stderr_buffer) |
582 | 589 |
|
| 590 | + async def _handle_agent_request(self, payload: dict[str, Any], session_id: str) -> bool: |
| 591 | + """Handle JSON-RPC requests initiated by the agent.""" |
| 592 | + method = payload.get("method") |
| 593 | + |
| 594 | + if method == "fs/read_text_file": |
| 595 | + await self._handle_fs_read_text_file(payload, session_id) |
| 596 | + return True |
| 597 | + |
| 598 | + return False |
| 599 | + |
| 600 | + async def _handle_fs_read_text_file(self, payload: dict[str, Any], session_id: str) -> None: |
| 601 | + """Execute the Codex filesystem read request via the bash tool.""" |
| 602 | + request_id = payload.get("id") |
| 603 | + params = payload.get("params") or {} |
| 604 | + |
| 605 | + if request_id is None: |
| 606 | + self._logger.warning("fs/read_text_file request missing id", extra={"payload": payload}) |
| 607 | + return |
| 608 | + |
| 609 | + self._logger.info("Handling fs/read_text_file request", extra={ |
| 610 | + "request_id": request_id, |
| 611 | + "path": params.get("path"), |
| 612 | + "line": params.get("line"), |
| 613 | + "limit": params.get("limit") |
| 614 | + }) |
| 615 | + |
| 616 | + # Resolve tool configuration |
| 617 | + tool = await get_tool("functions.acp_fs__read_text_file") |
| 618 | + if not tool: |
| 619 | + await self._write_json({ |
| 620 | + "jsonrpc": "2.0", |
| 621 | + "id": request_id, |
| 622 | + "error": { |
| 623 | + "code": -32001, |
| 624 | + "message": "Tool functions.acp_fs__read_text_file is not available" |
| 625 | + } |
| 626 | + }) |
| 627 | + return |
| 628 | + |
| 629 | + tool_params: dict[str, Any] = {} |
| 630 | + path = params.get("path") |
| 631 | + if not path: |
| 632 | + await self._write_json({ |
| 633 | + "jsonrpc": "2.0", |
| 634 | + "id": request_id, |
| 635 | + "error": { |
| 636 | + "code": -32602, |
| 637 | + "message": "Missing required parameter: path" |
| 638 | + } |
| 639 | + }) |
| 640 | + return |
| 641 | + tool_params["path"] = path |
| 642 | + |
| 643 | + # Optional parameters with validation |
| 644 | + if "line" in params and params["line"] is not None: |
| 645 | + try: |
| 646 | + tool_params["line"] = int(params["line"]) |
| 647 | + except (TypeError, ValueError): |
| 648 | + await self._write_json({ |
| 649 | + "jsonrpc": "2.0", |
| 650 | + "id": request_id, |
| 651 | + "error": { |
| 652 | + "code": -32602, |
| 653 | + "message": "Invalid line parameter; must be an integer" |
| 654 | + } |
| 655 | + }) |
| 656 | + return |
| 657 | + |
| 658 | + if "limit" in params and params["limit"] is not None: |
| 659 | + try: |
| 660 | + tool_params["limit"] = int(params["limit"]) |
| 661 | + except (TypeError, ValueError): |
| 662 | + await self._write_json({ |
| 663 | + "jsonrpc": "2.0", |
| 664 | + "id": request_id, |
| 665 | + "error": { |
| 666 | + "code": -32602, |
| 667 | + "message": "Invalid limit parameter; must be an integer" |
| 668 | + } |
| 669 | + }) |
| 670 | + return |
| 671 | + |
| 672 | + # Execute the tool |
| 673 | + executor = get_bash_executor() |
| 674 | + exec_session_id = params.get("sessionId") or session_id |
| 675 | + context = ExecutionContext( |
| 676 | + tool_id=tool.id, |
| 677 | + session_id=exec_session_id, |
| 678 | + task_id=f"fs_read_text_file_{uuid4().hex}", |
| 679 | + user_id="zedacp_user" |
| 680 | + ) |
| 681 | + |
| 682 | + result = await executor.execute_tool(tool, tool_params, context) |
| 683 | + |
| 684 | + if result.success: |
| 685 | + await self._write_json({ |
| 686 | + "jsonrpc": "2.0", |
| 687 | + "id": request_id, |
| 688 | + "result": { |
| 689 | + "content": result.output |
| 690 | + } |
| 691 | + }) |
| 692 | + self._logger.info("fs/read_text_file completed", extra={ |
| 693 | + "request_id": request_id, |
| 694 | + "path": path, |
| 695 | + "line": tool_params.get("line"), |
| 696 | + "limit": tool_params.get("limit"), |
| 697 | + "output_length": len(result.output) if result.output else 0 |
| 698 | + }) |
| 699 | + else: |
| 700 | + await self._write_json({ |
| 701 | + "jsonrpc": "2.0", |
| 702 | + "id": request_id, |
| 703 | + "error": { |
| 704 | + "code": -32002, |
| 705 | + "message": result.error or "Tool execution failed" |
| 706 | + } |
| 707 | + }) |
| 708 | + self._logger.error("fs/read_text_file failed", extra={ |
| 709 | + "request_id": request_id, |
| 710 | + "path": path, |
| 711 | + "error": result.error, |
| 712 | + "return_code": result.return_code |
| 713 | + }) |
| 714 | + |
583 | 715 | async def _process_zedacp_tool_calls(self, response: Dict[str, Any], session_id: str) -> Dict[str, Any]: |
584 | 716 | """Process ZedACP tool calls in agent response. |
585 | 717 |
|
|
0 commit comments