diff --git a/pyproject.toml b/pyproject.toml
index 3e0e5742..8503c2bd 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -78,6 +78,7 @@ include = [
testpaths = ["tests"]
pythonpath = ["src"]
asyncio_mode = "auto"
+asyncio_default_fixture_loop_scope = "function"
addopts = "-v --tb=short"
[tool.coverage.run]
diff --git a/src/dashboard/app.py b/src/dashboard/app.py
index 9c336130..78e7be2c 100644
--- a/src/dashboard/app.py
+++ b/src/dashboard/app.py
@@ -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)
diff --git a/src/dashboard/routes/tools.py b/src/dashboard/routes/tools.py
new file mode 100644
index 00000000..f8155e91
--- /dev/null
+++ b/src/dashboard/routes/tools.py
@@ -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()),
+ }
diff --git a/src/dashboard/templates/base.html b/src/dashboard/templates/base.html
index 0bbd228b..4d92db30 100644
--- a/src/dashboard/templates/base.html
+++ b/src/dashboard/templates/base.html
@@ -24,8 +24,9 @@
BRIEFING
SWARM
MARKET
+ TOOLS
MOBILE
- TEST
+
@@ -44,5 +45,6 @@
updateClock();
+