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:
Alexander Payne
2026-02-22 19:01:04 -05:00
parent c5f86b8960
commit f0aa43533f
17 changed files with 1628 additions and 13 deletions

View File

@@ -1,5 +1,6 @@
import asyncio
import logging
import os
from contextlib import asynccontextmanager
from pathlib import Path
@@ -21,6 +22,7 @@ from dashboard.routes.swarm_ws import router as swarm_ws_router
from dashboard.routes.briefing import router as briefing_router
from dashboard.routes.telegram import router as telegram_router
from dashboard.routes.swarm_internal import router as swarm_internal_router
from dashboard.routes.tools import router as tools_router
logging.basicConfig(
level=logging.INFO,
@@ -83,6 +85,18 @@ async def lifespan(app: FastAPI):
rec["agents_offlined"],
)
# Auto-spawn persona agents for a functional swarm (Echo, Forge, Seer)
# Skip auto-spawning in test mode to avoid test isolation issues
if os.environ.get("TIMMY_TEST_MODE") != "1":
logger.info("Auto-spawning persona agents: Echo, Forge, Seer...")
try:
swarm_coordinator.spawn_persona("echo", agent_id="persona-echo")
swarm_coordinator.spawn_persona("forge", agent_id="persona-forge")
swarm_coordinator.spawn_persona("seer", agent_id="persona-seer")
logger.info("Persona agents spawned successfully")
except Exception as exc:
logger.error("Failed to spawn persona agents: %s", exc)
# Auto-start Telegram bot if a token is configured
from telegram_bot.bot import telegram_bot
await telegram_bot.start()
@@ -121,6 +135,7 @@ app.include_router(swarm_ws_router)
app.include_router(briefing_router)
app.include_router(telegram_router)
app.include_router(swarm_internal_router)
app.include_router(tools_router)
@app.get("/", response_class=HTMLResponse)

View File

@@ -0,0 +1,92 @@
"""Tools dashboard route — /tools endpoints.
Provides a dashboard page showing available tools, which agents have access
to which tools, and usage statistics.
"""
from pathlib import Path
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
from swarm import registry as swarm_registry
from swarm.personas import PERSONAS
from timmy.tools import get_all_available_tools, get_tool_stats
router = APIRouter(tags=["tools"])
templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates"))
@router.get("/tools", response_class=HTMLResponse)
async def tools_page(request: Request):
"""Render the tools dashboard page."""
# Get all available tools
available_tools = get_all_available_tools()
# Get registered agents and their personas
agents = swarm_registry.list_agents()
agent_tools = []
for agent in agents:
# Determine which tools this agent has based on its capabilities/persona
tools_for_agent = []
# Check if it's a persona by name
persona_id = None
for pid, pdata in PERSONAS.items():
if pdata["name"].lower() == agent.name.lower():
persona_id = pid
break
if persona_id:
# Get tools for this persona
for tool_id, tool_info in available_tools.items():
if persona_id in tool_info["available_in"]:
tools_for_agent.append({
"id": tool_id,
"name": tool_info["name"],
"description": tool_info["description"],
})
elif agent.name.lower() == "timmy":
# Timmy has all tools
for tool_id, tool_info in available_tools.items():
tools_for_agent.append({
"id": tool_id,
"name": tool_info["name"],
"description": tool_info["description"],
})
# Get tool stats for this agent
stats = get_tool_stats(agent.id)
agent_tools.append({
"id": agent.id,
"name": agent.name,
"status": agent.status,
"tools": tools_for_agent,
"stats": stats,
})
# Calculate overall stats
total_calls = sum(a["stats"]["total_calls"] for a in agent_tools if a["stats"])
return templates.TemplateResponse(
request,
"tools.html",
{
"page_title": "Tools & Capabilities",
"available_tools": available_tools,
"agent_tools": agent_tools,
"total_calls": total_calls,
},
)
@router.get("/tools/api/stats")
async def tools_api_stats():
"""Return tool usage statistics as JSON."""
return {
"all_stats": get_tool_stats(),
"available_tools": list(get_all_available_tools().keys()),
}

View File

@@ -24,8 +24,9 @@
<a href="/briefing" class="mc-test-link">BRIEFING</a>
<a href="/swarm/live" class="mc-test-link">SWARM</a>
<a href="/marketplace/ui" class="mc-test-link">MARKET</a>
<a href="/tools" class="mc-test-link">TOOLS</a>
<a href="/mobile" class="mc-test-link">MOBILE</a>
<a href="/mobile-test" class="mc-test-link">TEST</a>
<button id="enable-notifications" class="mc-test-link" style="background:none;border:none;cursor:pointer;" title="Enable notifications">🔔</button>
<span class="mc-time" id="clock"></span>
</div>
</header>
@@ -44,5 +45,6 @@
updateClock();
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc4s9bIOgUxi8T/jzmE6bgx5xwkVYG3WhIEOFSjBqg4X" crossorigin="anonymous"></script>
<script src="/static/notifications.js"></script>
</body>
</html>

View File

@@ -0,0 +1,94 @@
{% extends "base.html" %}
{% block title %}Tools & Capabilities — Mission Control{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row mb-4">
<div class="col">
<h1 class="display-6">🔧 Tools & Capabilities</h1>
<p class="text-secondary">Agent tools and usage statistics</p>
</div>
<div class="col-auto">
<div class="card bg-dark border-secondary">
<div class="card-body text-center">
<h3 class="mb-0">{{ total_calls }}</h3>
<small class="text-secondary">Total Tool Calls</small>
</div>
</div>
</div>
</div>
<!-- Available Tools Reference -->
<div class="row mb-4">
<div class="col-12">
<h5 class="mb-3">Available Tools</h5>
<div class="row g-3">
{% for tool_id, tool_info in available_tools.items() %}
<div class="col-md-4">
<div class="card h-100 bg-dark border-secondary">
<div class="card-body">
<h6 class="card-title">{{ tool_info.name }}</h6>
<p class="card-text small text-secondary">{{ tool_info.description }}</p>
<div class="mt-2">
<small class="text-muted">Available to:</small>
<div class="d-flex flex-wrap gap-1 mt-1">
{% for persona in tool_info.available_in %}
<span class="badge bg-secondary">{{ persona|title }}</span>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Agent Tool Assignments -->
<div class="row">
<div class="col-12">
<h5 class="mb-3">Agent Capabilities</h5>
{% if agent_tools %}
<div class="row g-3">
{% for agent in agent_tools %}
<div class="col-md-6">
<div class="card bg-dark border-secondary">
<div class="card-header d-flex justify-content-between align-items-center">
<span>
<strong>{{ agent.name }}</strong>
<span class="badge {% if agent.status == 'idle' %}bg-success{% elif agent.status == 'busy' %}bg-warning{% else %}bg-secondary{% endif %} ms-2">
{{ agent.status }}
</span>
</span>
{% if agent.stats %}
<small class="text-muted">{{ agent.stats.total_calls }} calls</small>
{% endif %}
</div>
<div class="card-body">
{% if agent.tools %}
<div class="d-flex flex-wrap gap-2">
{% for tool in agent.tools %}
<span class="badge bg-primary" title="{{ tool.description }}">
{{ tool.name }}
</span>
{% endfor %}
</div>
{% else %}
<p class="text-secondary mb-0">No tools assigned</p>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-secondary">
No agents registered yet.
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}