#!/usr/bin/env python3 """Team Board Agent — Coder. Connects to Tracker via WebSocket, listens for tasks, executes them via OpenClaw. """ import asyncio import json import logging import signal import sys import websockets from config import ( TRACKER_WS_URL, AGENT_TOKEN, AGENT_NAME, AGENT_SLUG, HEARTBEAT_INTERVAL, LOBBY_CHAT_ID, ) from brain import think logging.basicConfig( level=logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", ) logger = logging.getLogger("agent") # State current_tasks: list[str] = [] status = "idle" running = True async def send_json(ws, data: dict): await ws.send(json.dumps(data)) async def heartbeat(ws): """Send periodic heartbeat to Tracker.""" while running: try: await send_json(ws, { "event": "agent.heartbeat", "status": status, "current_tasks": current_tasks, }) except Exception: break await asyncio.sleep(HEARTBEAT_INTERVAL) async def handle_task_assigned(ws, data: dict): """Handle an assigned task — execute it.""" global status task_id = data.get("task_id") task_title = data.get("title", "Без названия") task_description = data.get("description") project_name = data.get("project_name", "") logger.info("Task assigned: %s — %s", task_id, task_title) status = "busy" current_tasks.append(task_id) try: # Take the task await send_json(ws, {"event": "task.take", "task_id": task_id}) # Think (send to OpenClaw) result = await think(task_title, task_description, project_name) # Post result as comment await send_json(ws, { "event": "task.comment", "task_id": task_id, "content": result, "sender_type": "agent", "sender_name": AGENT_NAME, }) # Complete the task await send_json(ws, {"event": "task.complete", "task_id": task_id}) logger.info("Task completed: %s", task_id) except Exception as e: logger.error("Error processing task %s: %s", task_id, e) # Comment the error await send_json(ws, { "event": "task.comment", "task_id": task_id, "content": f"❌ Ошибка: {e}", "sender_type": "agent", "sender_name": AGENT_NAME, }) finally: if task_id in current_tasks: current_tasks.remove(task_id) status = "idle" if not current_tasks else "busy" async def handle_chat_message(ws, data: dict): """Handle incoming chat message — respond if mentioned.""" content = data.get("content", "") sender_name = data.get("sender_name", "") chat_id = data.get("chat_id") # Only respond if mentioned if f"@{AGENT_SLUG}" not in content and f"@{AGENT_NAME}" not in content.lower(): return logger.info("Mentioned in chat by %s: %s", sender_name, content[:100]) # Simple response via brain result = await think( f"Сообщение от {sender_name}: {content}", None, "чат", ) await send_json(ws, { "event": "chat.send", "chat_id": chat_id, "content": result, "sender_type": "agent", "sender_name": AGENT_NAME, }) async def connect(): """Main connection loop with auto-reconnect.""" while running: try: url = f"{TRACKER_WS_URL}?client_type=agent&client_id={AGENT_SLUG}" logger.info("Connecting to Tracker: %s", url) async with websockets.connect(url) as ws: logger.info("Connected to Tracker ✅") # Auth await send_json(ws, { "event": "auth", "token": AGENT_TOKEN, }) # Start heartbeat hb_task = asyncio.create_task(heartbeat(ws)) try: async for raw in ws: try: msg = json.loads(raw) except json.JSONDecodeError: continue event = msg.get("event") logger.debug("Event: %s", event) if event == "auth.ok": logger.info("Authenticated. Init: %s", json.dumps(msg.get("init", {}), ensure_ascii=False)[:200]) # Subscribe to lobby chat if LOBBY_CHAT_ID: await send_json(ws, {"event": "chat.subscribe", "chat_id": LOBBY_CHAT_ID}) logger.info("Subscribed to lobby chat: %s", LOBBY_CHAT_ID) elif event == "task.assigned": asyncio.create_task(handle_task_assigned(ws, msg)) elif event == "chat.message": asyncio.create_task(handle_chat_message(ws, msg)) elif event == "agent.heartbeat.ack": pass # OK elif event == "error": logger.warning("Error from Tracker: %s", msg.get("message")) else: logger.debug("Unhandled event: %s", event) finally: hb_task.cancel() except (websockets.ConnectionClosed, ConnectionRefusedError, OSError) as e: logger.warning("Connection lost: %s. Reconnecting in 5s...", e) await asyncio.sleep(5) except Exception as e: logger.error("Unexpected error: %s. Reconnecting in 10s...", e) await asyncio.sleep(10) def main(): global running logger.info("🤖 Agent '%s' (%s) starting...", AGENT_NAME, AGENT_SLUG) if not AGENT_TOKEN: logger.error("AGENT_TOKEN is required!") sys.exit(1) loop = asyncio.new_event_loop() def stop(*_): global running running = False logger.info("Shutting down...") loop.call_soon_threadsafe(loop.stop) signal.signal(signal.SIGINT, stop) signal.signal(signal.SIGTERM, stop) try: loop.run_until_complete(connect()) except RuntimeError: pass # loop stopped by signal finally: loop.close() if __name__ == "__main__": main()