mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-16 01:40:15 +08:00
- Add RuntimeStatusStar ABP plugin exposing runtime internal state - Add ACP echo server fixture for testing - Add ACP integration tests (TCP/Unix socket connectivity) - Update orchestrator to auto-register RuntimeStatusStar - Update tests to account for auto-registered star
138 lines
4.2 KiB
Python
138 lines
4.2 KiB
Python
"""
|
|
Simple ACP server that echoes back tool calls.
|
|
Used for testing the ACP client.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import sys
|
|
from typing import Any, Callable
|
|
|
|
|
|
class EchoAcpServer:
|
|
"""ACP echo server that responds to tool calls."""
|
|
|
|
def __init__(self) -> None:
|
|
self._connected = False
|
|
|
|
async def handle_request(self, message: dict[str, Any]) -> dict[str, Any]:
|
|
"""Handle an ACP request and return response."""
|
|
method = str(message.get("method", ""))
|
|
request_id = message.get("id")
|
|
|
|
if method == "initialize":
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"id": request_id,
|
|
"result": {"protocolVersion": "1.0", "capabilities": {}},
|
|
}
|
|
|
|
elif method == "echo":
|
|
params = message.get("params", {})
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"id": request_id,
|
|
"result": {"content": [{"type": "text", "text": f"echo: {params}"}]},
|
|
}
|
|
|
|
elif "/" in method:
|
|
# Handle server_name/tool_name format
|
|
parts = method.split("/")
|
|
server_name = parts[0] if parts else ""
|
|
tool_name = parts[1] if len(parts) > 1 else ""
|
|
params = message.get("params", {})
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"id": request_id,
|
|
"result": {
|
|
"content": [
|
|
{"type": "text", "text": f"{server_name}/{tool_name}({params})"}
|
|
]
|
|
},
|
|
}
|
|
|
|
return {
|
|
"jsonrpc": "2.0",
|
|
"id": request_id,
|
|
"error": {"code": -32601, "message": f"Method not found: {method}"},
|
|
}
|
|
|
|
async def handle_connection(
|
|
self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
|
) -> None:
|
|
"""Handle a client connection."""
|
|
self._connected = True
|
|
buffer = b""
|
|
try:
|
|
while True:
|
|
data = await reader.read(4096)
|
|
if not data:
|
|
break
|
|
buffer += data
|
|
|
|
while True:
|
|
header_end = buffer.find(b"\n")
|
|
if header_end == -1:
|
|
break
|
|
|
|
try:
|
|
header = json.loads(buffer[:header_end].decode("utf-8"))
|
|
except json.JSONDecodeError:
|
|
buffer = buffer[header_end + 1 :]
|
|
continue
|
|
|
|
content_length = header.get("content-length", 0)
|
|
if content_length == 0 or len(buffer) < header_end + 1 + content_length:
|
|
break
|
|
|
|
content = buffer[header_end + 1 : header_end + 1 + content_length]
|
|
buffer = buffer[header_end + 1 + content_length :]
|
|
|
|
try:
|
|
message = json.loads(content.decode("utf-8"))
|
|
except json.JSONDecodeError:
|
|
continue
|
|
|
|
response = await self.handle_request(message)
|
|
response_content = json.dumps(response)
|
|
response_header = json.dumps({"content-length": len(response_content)}) + "\n"
|
|
writer.write((response_header + response_content).encode())
|
|
await writer.drain()
|
|
|
|
except Exception:
|
|
pass
|
|
finally:
|
|
writer.close()
|
|
await writer.wait_closed()
|
|
self._connected = False
|
|
|
|
|
|
async def main() -> None:
|
|
"""Run the ACP echo server on a Unix socket."""
|
|
socket_path = "/tmp/test_acp_echo.sock"
|
|
|
|
# Remove existing socket file
|
|
if os.path.exists(socket_path):
|
|
os.remove(socket_path)
|
|
|
|
server = EchoAcpServer()
|
|
server_handle = await asyncio.start_server(
|
|
server.handle_connection, path=socket_path
|
|
)
|
|
|
|
addr = server_handle.sockets[0].getsockname()
|
|
print(f"ACP echo server listening on {addr}", flush=True)
|
|
|
|
try:
|
|
async with server_handle:
|
|
await server_handle.serve_forever()
|
|
except KeyboardInterrupt:
|
|
print("ACP echo server shutting down", flush=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|