forked from Rockachopa/Timmy-time-dashboard
feat: swarm E2E, MCP tools, timmy-serve L402, tests, notifications
Major Features: - Auto-spawn persona agents (Echo, Forge, Seer) on app startup - WebSocket broadcasts for real-time swarm UI updates - MCP tool integration: web search, file I/O, shell, Python execution - New /tools dashboard page showing agent capabilities - Real timmy-serve start with L402 payment gating middleware - Browser push notifications for briefings and task events Tests: - test_docker_agent.py: 9 tests for Docker agent runner - test_swarm_integration_full.py: 18 E2E lifecycle tests - Fixed all pytest warnings (436 tests, 0 warnings) Improvements: - Fixed coroutine warnings in coordinator broadcasts - Fixed ResourceWarning for unclosed process pipes - Added pytest-asyncio config to pyproject.toml - Test isolation with proper event loop cleanup
This commit is contained in:
@@ -94,6 +94,8 @@ class SwarmCoordinator:
|
||||
"Persona %s bid %d sats on task %s",
|
||||
node.name, bid_sats, task_id,
|
||||
)
|
||||
# Broadcast bid via WebSocket
|
||||
self._broadcast(self._broadcast_bid, task_id, aid, bid_sats)
|
||||
|
||||
self.comms.subscribe("swarm:tasks", _bid_and_register)
|
||||
|
||||
@@ -105,6 +107,10 @@ class SwarmCoordinator:
|
||||
)
|
||||
self._in_process_nodes.append(node)
|
||||
logger.info("Spawned persona %s (%s)", node.name, aid)
|
||||
|
||||
# Broadcast agent join via WebSocket
|
||||
self._broadcast(self._broadcast_agent_joined, aid, node.name)
|
||||
|
||||
return {
|
||||
"agent_id": aid,
|
||||
"name": node.name,
|
||||
@@ -177,6 +183,8 @@ class SwarmCoordinator:
|
||||
self.auctions.open_auction(task.id)
|
||||
self.comms.post_task(task.id, description)
|
||||
logger.info("Task posted: %s (%s)", task.id, description[:50])
|
||||
# Broadcast task posted via WebSocket
|
||||
self._broadcast(self._broadcast_task_posted, task.id, description)
|
||||
return task
|
||||
|
||||
async def run_auction_and_assign(self, task_id: str) -> Optional[Bid]:
|
||||
@@ -225,6 +233,8 @@ class SwarmCoordinator:
|
||||
"Task %s assigned to %s at %d sats",
|
||||
task_id, winner.agent_id, winner.bid_sats,
|
||||
)
|
||||
# Broadcast task assigned via WebSocket
|
||||
self._broadcast(self._broadcast_task_assigned, task_id, winner.agent_id)
|
||||
else:
|
||||
update_task(task_id, status=TaskStatus.FAILED)
|
||||
logger.warning("Task %s: no bids received, marked as failed", task_id)
|
||||
@@ -247,6 +257,11 @@ class SwarmCoordinator:
|
||||
self.comms.complete_task(task_id, task.assigned_agent, result)
|
||||
# Record success in learner
|
||||
swarm_learner.record_task_result(task_id, task.assigned_agent, succeeded=True)
|
||||
# Broadcast task completed via WebSocket
|
||||
self._broadcast(
|
||||
self._broadcast_task_completed,
|
||||
task_id, task.assigned_agent, result
|
||||
)
|
||||
return updated
|
||||
|
||||
def fail_task(self, task_id: str, reason: str = "") -> Optional[Task]:
|
||||
@@ -273,6 +288,65 @@ class SwarmCoordinator:
|
||||
def list_tasks(self, status: Optional[TaskStatus] = None) -> list[Task]:
|
||||
return list_tasks(status)
|
||||
|
||||
# ── WebSocket broadcasts ────────────────────────────────────────────────
|
||||
|
||||
def _broadcast(self, broadcast_fn, *args) -> None:
|
||||
"""Safely schedule a broadcast, handling sync/async contexts.
|
||||
|
||||
Only creates the coroutine and schedules it if an event loop is running.
|
||||
This prevents 'coroutine was never awaited' warnings in tests.
|
||||
"""
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
# Create coroutine only when we have an event loop
|
||||
coro = broadcast_fn(*args)
|
||||
asyncio.create_task(coro)
|
||||
except RuntimeError:
|
||||
# No event loop running - skip broadcast silently
|
||||
pass
|
||||
|
||||
async def _broadcast_agent_joined(self, agent_id: str, name: str) -> None:
|
||||
"""Broadcast agent joined event via WebSocket."""
|
||||
try:
|
||||
from websocket.handler import ws_manager
|
||||
await ws_manager.broadcast_agent_joined(agent_id, name)
|
||||
except Exception as exc:
|
||||
logger.debug("WebSocket broadcast failed (agent_joined): %s", exc)
|
||||
|
||||
async def _broadcast_bid(self, task_id: str, agent_id: str, bid_sats: int) -> None:
|
||||
"""Broadcast bid submitted event via WebSocket."""
|
||||
try:
|
||||
from websocket.handler import ws_manager
|
||||
await ws_manager.broadcast_bid_submitted(task_id, agent_id, bid_sats)
|
||||
except Exception as exc:
|
||||
logger.debug("WebSocket broadcast failed (bid): %s", exc)
|
||||
|
||||
async def _broadcast_task_posted(self, task_id: str, description: str) -> None:
|
||||
"""Broadcast task posted event via WebSocket."""
|
||||
try:
|
||||
from websocket.handler import ws_manager
|
||||
await ws_manager.broadcast_task_posted(task_id, description)
|
||||
except Exception as exc:
|
||||
logger.debug("WebSocket broadcast failed (task_posted): %s", exc)
|
||||
|
||||
async def _broadcast_task_assigned(self, task_id: str, agent_id: str) -> None:
|
||||
"""Broadcast task assigned event via WebSocket."""
|
||||
try:
|
||||
from websocket.handler import ws_manager
|
||||
await ws_manager.broadcast_task_assigned(task_id, agent_id)
|
||||
except Exception as exc:
|
||||
logger.debug("WebSocket broadcast failed (task_assigned): %s", exc)
|
||||
|
||||
async def _broadcast_task_completed(
|
||||
self, task_id: str, agent_id: str, result: str
|
||||
) -> None:
|
||||
"""Broadcast task completed event via WebSocket."""
|
||||
try:
|
||||
from websocket.handler import ws_manager
|
||||
await ws_manager.broadcast_task_completed(task_id, agent_id, result)
|
||||
except Exception as exc:
|
||||
logger.debug("WebSocket broadcast failed (task_completed): %s", exc)
|
||||
|
||||
# ── Convenience ─────────────────────────────────────────────────────────
|
||||
|
||||
def status(self) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user