From 4c35396702164128fda6c57eea0ef5793ad02955 Mon Sep 17 00:00:00 2001 From: Fred Porter Date: Wed, 19 Aug 2026 21:38:43 +0800 Subject: [PATCH] refactor: make MCP namespace truthful --- backend/app/api/dev_layer_api.py | 1 - backend/app/api/feed_api.py | 2 +- backend/app/api/hivemind_knowledge.py | 258 --------- backend/app/api/routes.py | 9 - backend/app/api/toon.py | 2 +- backend/app/core/settings.py | 2 +- backend/app/mcp/consensus.py | 246 -------- backend/app/mcp/feed/__init__.py | 10 - backend/app/mcp/hivemind_launcher.py | 110 ---- backend/app/mcp/hivemind_server.py | 547 ------------------ backend/app/mcp/llm_router.py | 282 --------- backend/app/mcp/roundtable_integration.py | 157 ----- backend/app/mcp/toon/__init__.py | 3 - backend/app/menu/unified_menu_simple.py | 2 - backend/app/services/control_service.py | 129 +---- .../feed_server.py => services/feed_store.py} | 16 +- backend/app/services/provider_router.py | 2 +- .../server.py => services/toon_context.py} | 6 +- .../skills/builtin/skill_nuggets_and_spool.py | 14 +- backend/app/utils/config_loader.py | 18 - backend/config/llm_router.yaml | 84 --- backend/tests/test_toon_server.py | 2 +- ...ASSISTUI_DEVELOPER_CHAT_LANE_SEPARATION.md | 4 +- docs/FEED_SYSTEM_SPEC.md | 8 +- docs/PLATES_SYSTEM_SPEC.md | 6 +- docs/SPOOL_SPEC.md | 4 +- scripts/demo_optimized_workflow.py | 7 +- scripts/ucore_watchdog.sh | 11 +- 28 files changed, 43 insertions(+), 1899 deletions(-) delete mode 100644 backend/app/api/hivemind_knowledge.py delete mode 100644 backend/app/mcp/consensus.py delete mode 100644 backend/app/mcp/feed/__init__.py delete mode 100644 backend/app/mcp/hivemind_launcher.py delete mode 100644 backend/app/mcp/hivemind_server.py delete mode 100644 backend/app/mcp/llm_router.py delete mode 100644 backend/app/mcp/roundtable_integration.py delete mode 100644 backend/app/mcp/toon/__init__.py rename backend/app/{mcp/feed/feed_server.py => services/feed_store.py} (91%) rename backend/app/{mcp/toon/server.py => services/toon_context.py} (98%) delete mode 100644 backend/config/llm_router.yaml diff --git a/backend/app/api/dev_layer_api.py b/backend/app/api/dev_layer_api.py index 16b2c45c..c531f568 100644 --- a/backend/app/api/dev_layer_api.py +++ b/backend/app/api/dev_layer_api.py @@ -164,7 +164,6 @@ def _get_capabilities_hud() -> dict: caps["developer"] = "online" if "ucore-developer" in running else "offline" caps["ollama"] = "online" if "ollama" in running else "offline" - caps["hivemind"] = "online" if "hivemind" in running else "offline" return caps diff --git a/backend/app/api/feed_api.py b/backend/app/api/feed_api.py index 41904f98..8762a5e0 100644 --- a/backend/app/api/feed_api.py +++ b/backend/app/api/feed_api.py @@ -20,7 +20,7 @@ def _get_feed_server(): global _feed_server if _feed_server is None: - from app.mcp.feed.feed_server import FeedServer + from app.services.feed_store import FeedServer _feed_server = FeedServer() return _feed_server diff --git a/backend/app/api/hivemind_knowledge.py b/backend/app/api/hivemind_knowledge.py deleted file mode 100644 index 23f5c9c4..00000000 --- a/backend/app/api/hivemind_knowledge.py +++ /dev/null @@ -1,258 +0,0 @@ -"""Hivemind Knowledge Layer API — Shared memory for multi-agent orchestration. - -Endpoints: - GET /api/hivemind/knowledge/status — Knowledge layer status - POST /api/hivemind/knowledge/publish — Publish a knowledge event - GET /api/hivemind/knowledge/query — Query knowledge events - GET /api/hivemind/knowledge/search — Full-text search - POST /api/hivemind/knowledge/subscribe — Subscribe an agent - GET /api/hivemind/knowledge/subscriptions — List subscriptions - POST /api/hivemind/knowledge/lock — Acquire a lock - POST /api/hivemind/knowledge/unlock — Release a lock - POST /api/hivemind/knowledge/cleanup — Cleanup expired events -""" -from __future__ import annotations - -import logging - -from aiohttp import web - -from app.services.knowledge_layer import KnowledgeLayer - -log = logging.getLogger("ucore.api.hivemind_knowledge") - - -async def handle_status(request: web.Request) -> web.Response: - """GET /api/hivemind/knowledge/status — Knowledge layer status.""" - kl = KnowledgeLayer() - status = await kl.status() - return web.json_response(status) - - -async def handle_publish(request: web.Request) -> web.Response: - """POST /api/hivemind/knowledge/publish — Publish a knowledge event. - - Body: - { - "agent_id": "agent.coder", - "event_type": "decision", - "payload": { ... }, - "ttl_seconds": 86400 - } - """ - try: - body = await request.json() - except Exception: - return web.json_response({"error": "Invalid JSON body"}, status=400) - - agent_id = body.get("agent_id", "") - event_type = body.get("event_type", "") - payload = body.get("payload", {}) - ttl = int(body.get("ttl_seconds", 86400)) - - if not agent_id or not event_type: - return web.json_response( - {"error": "agent_id and event_type are required"}, - status=400, - ) - - kl = KnowledgeLayer() - event_id = await kl.publish(agent_id, event_type, payload, ttl) - return web.json_response({"event_id": event_id, "status": "published"}) - - -async def handle_query(request: web.Request) -> web.Response: - """GET /api/hivemind/knowledge/query — Query knowledge events. - - Query params: - event_type: Filter by event type - agent_id: Filter by agent ID - limit: Max results (default: 50) - offset: Pagination offset (default: 0) - """ - kl = KnowledgeLayer() - event_type = request.query.get("event_type") - agent_id = request.query.get("agent_id") - limit = int(request.query.get("limit", 50)) - offset = int(request.query.get("offset", 0)) - - results = await kl.query( - event_type=event_type, - agent_id=agent_id, - limit=limit, - offset=offset, - ) - return web.json_response({ - "events": results, - "count": len(results), - }) - - -async def handle_search(request: web.Request) -> web.Response: - """GET /api/hivemind/knowledge/search — Full-text search. - - Query params: - q: Search query - limit: Max results (default: 20) - """ - query = request.query.get("q", "") - if not query: - return web.json_response( - {"error": "Query parameter 'q' is required"}, - status=400, - ) - - kl = KnowledgeLayer() - limit = int(request.query.get("limit", 20)) - results = await kl.search(query, limit) - return web.json_response({ - "events": results, - "count": len(results), - }) - - -async def handle_subscribe(request: web.Request) -> web.Response: - """POST /api/hivemind/knowledge/subscribe — Subscribe an agent. - - Body: - { - "agent_id": "agent.coder", - "event_type": "decision" - } - """ - try: - body = await request.json() - except Exception: - return web.json_response({"error": "Invalid JSON body"}, status=400) - - agent_id = body.get("agent_id", "") - event_type = body.get("event_type") - - if not agent_id: - return web.json_response( - {"error": "agent_id is required"}, - status=400, - ) - - kl = KnowledgeLayer() - sub_id = await kl.subscribe(agent_id, event_type) - return web.json_response({"subscription_id": sub_id}) - - -async def handle_subscriptions(request: web.Request) -> web.Response: - """GET /api/hivemind/knowledge/subscriptions — List subscriptions. - - Query params: - agent_id: Optional filter by agent - """ - kl = KnowledgeLayer() - agent_id = request.query.get("agent_id") - subs = await kl.get_subscriptions(agent_id) - return web.json_response({ - "subscriptions": subs, - "count": len(subs), - }) - - -async def handle_lock(request: web.Request) -> web.Response: - """POST /api/hivemind/knowledge/lock — Acquire a lock. - - Body: - { - "resource_id": "resource-123", - "agent_id": "agent.coder" - } - """ - try: - body = await request.json() - except Exception: - return web.json_response({"error": "Invalid JSON body"}, status=400) - - resource_id = body.get("resource_id", "") - agent_id = body.get("agent_id", "") - - if not resource_id or not agent_id: - return web.json_response( - {"error": "resource_id and agent_id are required"}, - status=400, - ) - - kl = KnowledgeLayer() - acquired = await kl.lock(resource_id, agent_id) - if acquired: - return web.json_response({"status": "locked", "resource_id": resource_id}) - return web.json_response( - {"error": "Resource already locked", "resource_id": resource_id}, - status=409, - ) - - -async def handle_unlock(request: web.Request) -> web.Response: - """POST /api/hivemind/knowledge/unlock — Release a lock. - - Body: - { - "resource_id": "resource-123", - "agent_id": "agent.coder" - } - """ - try: - body = await request.json() - except Exception: - return web.json_response({"error": "Invalid JSON body"}, status=400) - - resource_id = body.get("resource_id", "") - agent_id = body.get("agent_id", "") - - if not resource_id or not agent_id: - return web.json_response( - {"error": "resource_id and agent_id are required"}, - status=400, - ) - - kl = KnowledgeLayer() - released = await kl.unlock(resource_id, agent_id) - if released: - return web.json_response({"status": "unlocked", "resource_id": resource_id}) - return web.json_response( - {"error": "Not locked by this agent", "resource_id": resource_id}, - status=403, - ) - - -async def handle_cleanup(request: web.Request) -> web.Response: - """POST /api/hivemind/knowledge/cleanup — Cleanup expired events.""" - kl = KnowledgeLayer() - removed = await kl.cleanup_expired() - return web.json_response({"removed": removed}) - - -def setup_routes(app: web.Application) -> None: - """Setup Hivemind knowledge layer API routes.""" - app.router.add_get( - "/api/hivemind/knowledge/status", handle_status, - ) - app.router.add_post( - "/api/hivemind/knowledge/publish", handle_publish, - ) - app.router.add_get( - "/api/hivemind/knowledge/query", handle_query, - ) - app.router.add_get( - "/api/hivemind/knowledge/search", handle_search, - ) - app.router.add_post( - "/api/hivemind/knowledge/subscribe", handle_subscribe, - ) - app.router.add_get( - "/api/hivemind/knowledge/subscriptions", handle_subscriptions, - ) - app.router.add_post( - "/api/hivemind/knowledge/lock", handle_lock, - ) - app.router.add_post( - "/api/hivemind/knowledge/unlock", handle_unlock, - ) - app.router.add_post( - "/api/hivemind/knowledge/cleanup", handle_cleanup, - ) diff --git a/backend/app/api/routes.py b/backend/app/api/routes.py index 766b127d..e036700d 100644 --- a/backend/app/api/routes.py +++ b/backend/app/api/routes.py @@ -460,15 +460,6 @@ def register_routes(app: web.Application) -> None: except ImportError as e: log.debug("Catalog routes not available: %s", e) - # ── Hivemind Knowledge Layer ───────────────────────────────────── - try: - from .hivemind_knowledge import setup_routes as setup_hivemind_knowledge_routes - - setup_hivemind_knowledge_routes(app) - log.debug("Hivemind knowledge layer routes registered") - except ImportError as e: - log.debug("Hivemind knowledge routes not available: %s", e) - # ── Dev Layer API (Dev Mode toggle) ───────────────────────────── try: from .dev_layer_api import register_dev_layer_routes diff --git a/backend/app/api/toon.py b/backend/app/api/toon.py index 844332b5..11b8b340 100644 --- a/backend/app/api/toon.py +++ b/backend/app/api/toon.py @@ -4,7 +4,7 @@ from aiohttp import web -from app.mcp.toon import TOONContextServer +from app.services.toon_context import TOONContextServer log = logging.getLogger("ucore.api.toon") diff --git a/backend/app/core/settings.py b/backend/app/core/settings.py index 0dd1d875..3027b4cd 100644 --- a/backend/app/core/settings.py +++ b/backend/app/core/settings.py @@ -1,4 +1,4 @@ -"""uCore settings — unified configuration for snackbar + hivemind + API""" +"""uCore settings — unified configuration for Snackbar and the API.""" from __future__ import annotations import os diff --git a/backend/app/mcp/consensus.py b/backend/app/mcp/consensus.py deleted file mode 100644 index 25fe6c9e..00000000 --- a/backend/app/mcp/consensus.py +++ /dev/null @@ -1,246 +0,0 @@ -"""Hivemind Consensus Engine — Multi-agent voting and resolution. - -Supports four voting modes: - - majority: Simple majority wins - - unanimous: All agents must agree - - weighted: Weighted score threshold - - escalation: Escalate to human on deadlock -""" -from __future__ import annotations - -import logging -from dataclasses import dataclass, field -from enum import Enum -from typing import Any - -log = logging.getLogger("ucore.mcp.hivemind.consensus") - - -class Vote(Enum): - APPROVE = "approve" - REJECT = "reject" - ABSTAIN = "abstain" - - -class ConsensusMode(Enum): - MAJORITY = "majority" - UNANIMOUS = "unanimous" - WEIGHTED = "weighted" - ESCALATION = "escalation" - - -@dataclass -class AgentVote: - agent_id: str - vote: Vote - reasoning: str = "" - weight: float = 1.0 - - -@dataclass -class Proposal: - id: str - agent_id: str - title: str - description: str - actions: list[dict[str, Any]] = field(default_factory=list) - votes: list[AgentVote] = field(default_factory=list) - round: int = 1 - max_rounds: int = 3 - resolved: bool = False - outcome: str | None = None - - -class ConsensusEngine: - """Multi-agent consensus engine with configurable voting modes.""" - - def __init__(self, mode: ConsensusMode = ConsensusMode.MAJORITY): - self.mode = mode - self.proposals: dict[str, Proposal] = {} - self._round_counters: dict[str, int] = {} - - def create_proposal( - self, - agent_id: str, - title: str, - description: str, - actions: list[dict[str, Any]] | None = None, - ) -> Proposal: - """Create a new proposal for agents to vote on.""" - proposal_id = f"prop_{len(self.proposals) + 1}" - proposal = Proposal( - id=proposal_id, - agent_id=agent_id, - title=title, - description=description, - actions=actions or [], - ) - self.proposals[proposal_id] = proposal - self._round_counters[proposal_id] = 1 - log.info("Proposal created: %s by %s — %s", proposal_id, agent_id, title) - return proposal - - def cast_vote( - self, - proposal_id: str, - agent_id: str, - vote: Vote, - reasoning: str = "", - weight: float = 1.0, - ) -> dict[str, Any]: - """Cast a vote on a proposal and check for resolution.""" - proposal = self.proposals.get(proposal_id) - if not proposal: - return {"error": f"Proposal '{proposal_id}' not found"} - - if proposal.resolved: - return {"error": "Proposal already resolved", "outcome": proposal.outcome} - - agent_vote = AgentVote( - agent_id=agent_id, - vote=vote, - reasoning=reasoning, - weight=weight, - ) - proposal.votes.append(agent_vote) - log.info("Vote cast: %s voted %s on %s", agent_id, vote.value, proposal_id) - - return self._check_resolution(proposal) - - def _check_resolution(self, proposal: Proposal) -> dict[str, Any]: - """Check if the proposal can be resolved based on current votes.""" - if not proposal.votes: - return {"status": "pending", "round": proposal.round} - - if self.mode == ConsensusMode.MAJORITY: - return self._resolve_majority(proposal) - elif self.mode == ConsensusMode.UNANIMOUS: - return self._resolve_unanimous(proposal) - elif self.mode == ConsensusMode.WEIGHTED: - return self._resolve_weighted(proposal) - elif self.mode == ConsensusMode.ESCALATION: - return self._resolve_escalation(proposal) - else: - return self._resolve_majority(proposal) - - def _resolve_majority(self, proposal: Proposal) -> dict[str, Any]: - """Simple majority: >50% of non-abstain votes wins.""" - approves = sum(1 for v in proposal.votes if v.vote == Vote.APPROVE) - rejects = sum(1 for v in proposal.votes if v.vote == Vote.REJECT) - total = approves + rejects - - if total == 0: - return {"status": "pending", "round": proposal.round} - - if approves > rejects: - return self._resolve(proposal, "approved") - elif rejects > approves: - return self._resolve(proposal, "rejected") - else: - return self._handle_tie(proposal) - - def _resolve_unanimous(self, proposal: Proposal) -> dict[str, Any]: - """Unanimous: all non-abstain votes must approve.""" - rejects = [v for v in proposal.votes if v.vote == Vote.REJECT] - if rejects: - return self._resolve(proposal, "rejected") - approves = [v for v in proposal.votes if v.vote == Vote.APPROVE] - if approves: - return self._resolve(proposal, "approved") - return {"status": "pending", "round": proposal.round} - - def _resolve_weighted(self, proposal: Proposal) -> dict[str, Any]: - """Weighted: score threshold (sum of weights * vote).""" - threshold = 0.5 # 50% threshold - total_weight = sum(v.weight for v in proposal.votes if v.vote != Vote.ABSTAIN) - approve_weight = sum( - v.weight for v in proposal.votes if v.vote == Vote.APPROVE - ) - - if total_weight == 0: - return {"status": "pending", "round": proposal.round} - - score = approve_weight / total_weight - if score > threshold: - return self._resolve(proposal, "approved") - elif score < threshold: - return self._resolve(proposal, "rejected") - else: - return self._handle_tie(proposal) - - def _resolve_escalation(self, proposal: Proposal) -> dict[str, Any]: - """Escalation: escalate to human on deadlock or after max rounds.""" - if proposal.round >= proposal.max_rounds: - return self._resolve( - proposal, "escalated", - detail="Max rounds reached, escalated to human", - ) - return self._resolve_majority(proposal) - - def _handle_tie(self, proposal: Proposal) -> dict[str, Any]: - """Handle a tie by incrementing round or escalating.""" - if proposal.round >= proposal.max_rounds: - return self._resolve( - proposal, "escalated", - detail="Tie after max rounds, escalated to human", - ) - proposal.round += 1 - log.info("Tie on %s — advancing to round %d", proposal.id, proposal.round) - return { - "status": "tie", - "round": proposal.round, - "message": f"Tie broken — advancing to round {proposal.round}", - } - - def _resolve( - self, - proposal: Proposal, - outcome: str, - detail: str | None = None, - ) -> dict[str, Any]: - """Mark a proposal as resolved.""" - proposal.resolved = True - proposal.outcome = outcome - log.info("Proposal %s resolved: %s", proposal.id, outcome) - result = { - "status": "resolved", - "outcome": outcome, - "round": proposal.round, - "votes": [ - {"agent_id": v.agent_id, "vote": v.vote.value, "reasoning": v.reasoning} - for v in proposal.votes - ], - } - if detail: - result["detail"] = detail - return result - - def get_status(self, proposal_id: str) -> dict[str, Any] | None: - """Get the current status of a proposal.""" - proposal = self.proposals.get(proposal_id) - if not proposal: - return None - return { - "id": proposal.id, - "agent_id": proposal.agent_id, - "title": proposal.title, - "round": proposal.round, - "resolved": proposal.resolved, - "outcome": proposal.outcome, - "vote_count": len(proposal.votes), - "votes": [ - {"agent_id": v.agent_id, "vote": v.vote.value} - for v in proposal.votes - ], - } - - def list_active(self) -> list[dict[str, Any]]: - """List all unresolved proposals.""" - result: list[dict[str, Any]] = [] - for pid, p in self.proposals.items(): - if not p.resolved: - status = self.get_status(pid) - if status: - result.append(status) - return result - diff --git a/backend/app/mcp/feed/__init__.py b/backend/app/mcp/feed/__init__.py deleted file mode 100644 index 3c7e13bc..00000000 --- a/backend/app/mcp/feed/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Feed MCP Server — unified incoming data layer for user activity. - -Provides ingest, query, suggest, and link tools for the Activity Pod (SQLite). -Bridges browser history, email, messages, alerts, and search activity into -the Spool, Tasker, and Binder systems. -""" - -from .feed_server import FeedServer - -__all__ = ["FeedServer"] diff --git a/backend/app/mcp/hivemind_launcher.py b/backend/app/mcp/hivemind_launcher.py deleted file mode 100644 index 0980635d..00000000 --- a/backend/app/mcp/hivemind_launcher.py +++ /dev/null @@ -1,110 +0,0 @@ -"""Shared Hivemind launcher — single source of truth for starting Hivemind. - -Used by both ``__main__.py`` (startup) and ``control_service.py`` (recovery). -""" -from __future__ import annotations - -import atexit -import os -import socket -import subprocess -import sys -import time -from pathlib import Path - -from app.core.logging import log - - -def _port_in_use(host: str, port: int) -> bool: - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock: - sock.settimeout(0.2) - return sock.connect_ex((host, port)) == 0 - - -def _health_is_ready(host: str, port: int) -> bool: - import urllib.request - try: - with urllib.request.urlopen( - f"http://{host}:{port}/health", - timeout=1.0, - ) as response: - return response.status == 200 - except Exception: - return False - - -def start_hivemind() -> None: - """Launch Hivemind MCP server as a background child process. - - Hivemind hosts the governed agent and consensus service. Provider and - budget selection remain behind the canonical Flow Router rather than - separate executable Skill wrappers. - - If Hivemind is already running and healthy, attaches silently. - If the port is occupied but unhealthy, skips auto-start. - Spawned as a child process with atexit cleanup. - """ - backend_root = Path(__file__).resolve().parents[2] - agents_config = backend_root / "config" / "agents.yaml" - llm_config = backend_root / "config" / "llm_router.yaml" - host = os.environ.get("UCORE_HIVEMIND_HOST", "127.0.0.1") - port = int(os.environ.get("UCORE_HIVEMIND_PORT", "8490")) - - if _port_in_use(host, port): - if _health_is_ready(host, port): - log.info( - "Hivemind already running on %s:%d; attaching", - host, - port, - ) - return - log.warning( - "Hivemind port %s:%d is occupied but unhealthy; " - "skipping auto-start", - host, - port, - ) - return - - try: - proc = subprocess.Popen( - [ - sys.executable, "-m", "app.mcp.hivemind_server", - "--host", host, - "--port", str(port), - "--agents-config", str(agents_config), - "--llm-config", str(llm_config), - ], - cwd=str(backend_root), - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - - # Verify child process actually booted and reached health endpoint. - for _ in range(30): - if proc.poll() is not None: - log.warning( - "Hivemind exited during startup (code %s); " - "skills depending on it will be unavailable", - proc.returncode, - ) - return - if _health_is_ready(host, port): - atexit.register(proc.terminate) - log.info( - "Hivemind auto-started (PID %d) on %s:%d", - proc.pid, - host, - port, - ) - return - time.sleep(0.1) - - atexit.register(proc.terminate) - log.warning( - "Hivemind process launched (PID %d) " - "but health did not report ready yet", - proc.pid, - ) - except Exception as exc: - log.error("Failed to start Hivemind: %s", exc) diff --git a/backend/app/mcp/hivemind_server.py b/backend/app/mcp/hivemind_server.py deleted file mode 100644 index fe613893..00000000 --- a/backend/app/mcp/hivemind_server.py +++ /dev/null @@ -1,547 +0,0 @@ -"""Hivemind MCP Server — Multi-agent orchestration server. - -Runs as a standalone aiohttp server on port 8490, exposing: - - Agent management (list, describe, route) - - Consensus engine (propose, vote, resolve) - - LLM routing (chat completion via Roundtable/Ollama/OpenAI) - - Workflow execution (run multi-step agent workflows) - -Usage: - python -m app.mcp.hivemind_server --port 8490 -""" -from __future__ import annotations - -import argparse -import json -import logging -import os -import sys - -from aiohttp import web - -from app.mcp.consensus import ConsensusEngine, Vote -from app.mcp.llm_router import LLMRouter -from app.mcp.roundtable_integration import get_roundtable -from app.secret.store import get_store -from app.services.agent_specialization import SpecializedAgentRegistry -from app.services.budget_manager import BudgetManager -from app.services.model_pricing import summarize_tiers - -log = logging.getLogger("ucore.mcp.hivemind") - - -# ─── Hivemind Server ─────────────────────────────────────── - - -def _cost_status_payload() -> dict: - """Build the budget/cost status payload from BudgetManager + model tiers.""" - status = BudgetManager.get().get_status() - - def _window(key: str) -> dict: - return { - "used": status[key]["spend"], - "limit": status[key]["budget"], - "remaining": status[key]["remaining"], - } - - return { - "daily": _window("daily"), - "weekly": {"used": 0.0, "limit": 0.0, "remaining": 0.0}, - "monthly": _window("monthly"), - "session": status["session"], - "circuit_breaker_open": status["circuit_breaker_open"], - "free_tier_only": status["free_tier_only"], - "per_agent": status["per_agent"], - "models": summarize_tiers(), - "top_models": [], - } - - -class HivemindServer: - """Hivemind MCP server — multi-agent orchestration.""" - - def __init__( - self, - agents_config: str | None = None, - llm_config: str | None = None, - ): - # Use the canonical SpecializedAgentRegistry from agent_specialization.py - self.registry = SpecializedAgentRegistry(agents_config) - self.consensus = ConsensusEngine() - # Wire LLM router to use Secret Store for API keys - self.llm_router = LLMRouter(llm_config) - self._store = get_store() - self.app = web.Application() - self._setup_routes() - - def _setup_routes(self): - """Set up HTTP routes.""" - self.app.router.add_get("/health", self.handle_health) - self.app.router.add_get("/api/hivemind/agents", self.handle_list_agents) - self.app.router.add_get( - "/api/hivemind/agents/{agent_id}", self.handle_get_agent - ) - self.app.router.add_post( - "/api/hivemind/route", self.handle_route_task - ) - self.app.router.add_get( - "/api/hivemind/workflows", self.handle_list_workflows - ) - self.app.router.add_get( - "/api/hivemind/workflows/{workflow_id}", self.handle_get_workflow - ) - self.app.router.add_post( - "/api/hivemind/consensus/propose", self.handle_propose - ) - self.app.router.add_post( - "/api/hivemind/consensus/vote", self.handle_vote - ) - self.app.router.add_get( - "/api/hivemind/consensus/{proposal_id}", self.handle_proposal_status - ) - self.app.router.add_get( - "/api/hivemind/consensus", self.handle_list_proposals - ) - self.app.router.add_post( - "/api/hivemind/chat", self.handle_chat - ) - self.app.router.add_get( - "/api/hivemind/llm/health", self.handle_llm_health - ) - self.app.router.add_post( - "/api/hivemind/workflow/run", self.handle_run_workflow - ) - self.app.router.add_get( - "/api/hivemind/roundtable", self.handle_roundtable_status - ) - self.app.router.add_get( - "/api/hivemind/llm/cost", self.handle_cost_status - ) - - async def handle_health(self, request: web.Request) -> web.Response: - """GET /health — Health check.""" - rt = get_roundtable() - return web.json_response({ - "status": "ok", - "service": "hivemind", - "version": "1.0.0", - "agents": len(self.registry.agents), - "active_proposals": len(self.consensus.list_active()), - "roundtable": rt.summary(), - "cost": _cost_status_payload(), - }) - - async def handle_cost_status( - self, request: web.Request - ) -> web.Response: - """GET /api/hivemind/llm/cost — Cost management status.""" - return web.json_response(_cost_status_payload()) - - async def handle_list_agents(self, request: web.Request) -> web.Response: - """GET /api/hivemind/agents — List all agents.""" - agents = self.registry.list_agents() - return web.json_response({ - "agents": [ - { - "id": a.id, - "name": a.name, - "model": a.model, - "provider": a.provider, - "capabilities": a.capabilities, - "cost_per_task": a.cost_per_task, - "timeout": a.timeout, - "priority": a.priority, - } - for a in agents - ], - }) - - async def handle_get_agent(self, request: web.Request) -> web.Response: - """GET /api/hivemind/agents/{agent_id} — Get agent details.""" - agent_id = request.match_info["agent_id"] - agent = self.registry.get_agent(agent_id) - if not agent: - return web.json_response( - {"error": f"Agent '{agent_id}' not found"}, - status=404, - ) - return web.json_response({ - "agent": { - "id": agent.id, - "name": agent.name, - "description": agent.description, - "model": agent.model, - "provider": agent.provider, - "capabilities": agent.capabilities, - "cost_per_task": agent.cost_per_task, - "timeout": agent.timeout, - "priority": agent.priority, - } - }) - - async def handle_route_task(self, request: web.Request) -> web.Response: - """POST /api/hivemind/route — Route a task to the best agent.""" - body = await request.json() - task_type = body.get("task_type", "") - complexity = body.get("complexity", "medium") - try: - agent = self.registry.get_best_agent_for_task(task_type, complexity) - except RuntimeError: - return web.json_response( - {"error": f"No agent found for task type '{task_type}'"}, - status=404, - ) - return web.json_response({ - "agent": { - "id": agent.id, - "name": agent.name, - "model": agent.model, - "provider": agent.provider, - "capabilities": agent.capabilities, - "timeout": agent.timeout, - } - }) - - async def handle_list_workflows(self, request: web.Request) -> web.Response: - """GET /api/hivemind/workflows — List all workflows.""" - workflows = [] - for wid, wt in self.registry.workflow_templates.items(): - workflows.append({ - "id": wid, - "name": wt.name, - "description": wt.description, - "stages": [ - {"role": s.role, "title": s.title} - for s in wt.stages - ], - }) - return web.json_response({"workflows": workflows}) - - async def handle_get_workflow(self, request: web.Request) -> web.Response: - """GET /api/hivemind/workflows/{workflow_id} — Get workflow details.""" - workflow_id = request.match_info["workflow_id"] - wt = self.registry.workflow_templates.get(workflow_id) - if not wt: - return web.json_response( - {"error": f"Workflow '{workflow_id}' not found"}, - status=404, - ) - return web.json_response({ - "workflow": { - "id": wt.id, - "name": wt.name, - "description": wt.description, - "task_types": wt.task_types, - "stages": [ - {"role": s.role, "title": s.title, "description": s.description} - for s in wt.stages - ], - } - }) - - async def handle_propose(self, request: web.Request) -> web.Response: - """POST /api/hivemind/consensus/propose — Create a new proposal.""" - body = await request.json() - agent_id = body.get("agent_id", "") - title = body.get("title", "") - description = body.get("description", "") - actions = body.get("actions", []) - - if not agent_id or not title: - return web.json_response( - {"error": "agent_id and title are required"}, - status=400, - ) - - proposal = self.consensus.create_proposal( - agent_id=agent_id, - title=title, - description=description, - actions=actions, - ) - return web.json_response({ - "proposal": { - "id": proposal.id, - "agent_id": proposal.agent_id, - "title": proposal.title, - "round": proposal.round, - } - }) - - async def handle_vote(self, request: web.Request) -> web.Response: - """POST /api/hivemind/consensus/vote — Cast a vote on a proposal.""" - body = await request.json() - proposal_id = body.get("proposal_id", "") - agent_id = body.get("agent_id", "") - vote_str = body.get("vote", "") - reasoning = body.get("reasoning", "") - weight = body.get("weight", 1.0) - - if not proposal_id or not agent_id or not vote_str: - return web.json_response( - {"error": "proposal_id, agent_id, and vote are required"}, - status=400, - ) - - try: - vote = Vote(vote_str) - except ValueError: - return web.json_response( - {"error": f"Invalid vote '{vote_str}'. Use: approve, reject, abstain"}, - status=400, - ) - - result = self.consensus.cast_vote( - proposal_id=proposal_id, - agent_id=agent_id, - vote=vote, - reasoning=reasoning, - weight=float(weight), - ) - return web.json_response(result) - - async def handle_proposal_status(self, request: web.Request) -> web.Response: - """GET /api/hivemind/consensus/{proposal_id} — Get proposal status.""" - proposal_id = request.match_info["proposal_id"] - status = self.consensus.get_status(proposal_id) - if not status: - return web.json_response( - {"error": f"Proposal '{proposal_id}' not found"}, - status=404, - ) - return web.json_response(status) - - async def handle_list_proposals(self, request: web.Request) -> web.Response: - """GET /api/hivemind/consensus — List all active proposals.""" - return web.json_response({ - "active_proposals": self.consensus.list_active(), - "mode": self.consensus.mode.value, - }) - - async def handle_chat(self, request: web.Request) -> web.Response: - """POST /api/hivemind/chat — Send a chat completion request.""" - body = await request.json() - model = body.get("model", "") - messages = body.get("messages", []) - system_prompt = body.get("system_prompt") - temperature = body.get("temperature", 0.7) - max_tokens = body.get("max_tokens", 4096) - - if not model or not messages: - return web.json_response( - {"error": "model and messages are required"}, - status=400, - ) - - result = await self.llm_router.chat_completion( - model=model, - messages=messages, - system_prompt=system_prompt, - temperature=float(temperature), - max_tokens=int(max_tokens), - ) - return web.json_response(result) - - async def handle_roundtable_status( - self, request: web.Request - ) -> web.Response: - """GET /api/hivemind/roundtable — Roundtable AI status.""" - rt = get_roundtable() - return web.json_response(rt.summary()) - - async def handle_llm_health(self, request: web.Request) -> web.Response: - """GET /api/hivemind/llm/health — Check LLM backend health.""" - results = await self.llm_router.health_check() - return web.json_response({"backends": results}) - - async def handle_run_workflow(self, request: web.Request) -> web.Response: - """POST /api/hivemind/workflow/run — Execute a multi-step workflow.""" - body = await request.json() - workflow_id = body.get("workflow_id", "") - context = body.get("context", {}) - - wt = self.registry.workflow_templates.get(workflow_id) - if not wt: - return web.json_response( - {"error": f"Workflow '{workflow_id}' not found"}, - status=404, - ) - - # Use LangGraph for agent orchestration if available - try: - return await self._run_workflow_langgraph(wt, context, workflow_id) - except ImportError: - log.info("LangGraph not available — using native execution") - return await self._run_workflow_native(wt, context, workflow_id) - - async def _run_workflow_langgraph(self, wt, context, workflow_id): - """Execute workflow using LangGraph StateGraph.""" - from typing import Annotated, TypedDict - - from langgraph.graph import END, StateGraph - from langgraph.graph.message import add_messages - - class WorkflowState(TypedDict): - messages: Annotated[list, add_messages] - stage_results: list[dict] - - builder = StateGraph(WorkflowState) - - for i, stage in enumerate(wt.stages): - agent = self.registry.get_agent(stage.role) - - stage_role = stage.role - - def make_node(agent_obj, stage_desc, stage_title, stage_role_val, ctx): - async def node_fn(state: WorkflowState) -> dict: - from litellm import acompletion - msgs = [{"role": "user", "content": stage_desc}] - if ctx.get("input"): - msgs.append({ - "role": "user", - "content": f"Context: {json.dumps(ctx['input'])}", - }) - try: - resp = await acompletion( - model=agent_obj.model if agent_obj else "ollama/qwen2.5-coder:3b", - messages=msgs, - ) - content = resp.choices[0].message.content - return { - "messages": [{"role": "assistant", "content": content}], - "stage_results": [{ - "stage": stage_title, - "role": stage_role_val, - "agent": agent_obj.name if agent_obj else "default", - "result": content, - }], - } - except Exception as exc: - return { - "messages": [], - "stage_results": [{ - "stage": stage_title, - "role": stage_role_val, - "error": str(exc), - }], - } - return node_fn - - node_fn = make_node(agent, stage.description, stage.title, stage_role, context) - node_name = f"stage_{i}" - builder.add_node(node_name, node_fn) - if i == 0: - builder.set_entry_point(node_name) - else: - builder.add_edge(f"stage_{i-1}", node_name) - - builder.add_edge(f"stage_{len(wt.stages) - 1}", END) - graph = builder.compile() - result = await graph.ainvoke({"messages": [], "stage_results": []}) - - return web.json_response({ - "workflow_id": workflow_id, - "engine": "langgraph", - "stages_completed": len(result.get("stage_results", [])), - "results": result.get("stage_results", []), - }) - - async def _run_workflow_native(self, wt, context, workflow_id): - """Fallback native workflow execution.""" - from app.services.provider_router import get_router - router = get_router() - results = [] - for stage in wt.stages: - agent = self.registry.get_agent(stage.role) - if not agent: - results.append({ - "stage": stage.title, - "role": stage.role, - "error": f"No agent found for role '{stage.role}'", - }) - continue - messages = [{"role": "user", "content": stage.description}] - if context.get("input"): - messages.append({ - "role": "user", - "content": f"Context: {json.dumps(context['input'])}", - }) - llm_result = await router.chat( - messages=messages, - model=agent.model, - ) - results.append({ - "stage": stage.title, - "role": stage.role, - "agent": agent.name, - "result": llm_result.get("content", ""), - }) - return web.json_response({ - "workflow_id": workflow_id, - "engine": "native", - "stages_completed": len(results), - "results": results, - }) - - -# ─── Main ────────────────────────────────────────────────── - -def main(): - parser = argparse.ArgumentParser( - description="Hivemind MCP Server — Multi-agent orchestration" - ) - parser.add_argument( - "--port", type=int, default=8490, - help="Port to listen on (default: 8490)", - ) - parser.add_argument( - "--host", type=str, default="127.0.0.1", - help="Host to bind to (default: 127.0.0.1)", - ) - parser.add_argument( - "--agents-config", type=str, default=None, - help="Path to agents.yaml config file", - ) - parser.add_argument( - "--llm-config", type=str, default=None, - help="Path to LLM router config file", - ) - parser.add_argument( - "--debug", action="store_true", - help="Enable debug logging", - ) - args = parser.parse_args() - - logging.basicConfig( - level=logging.DEBUG if args.debug else logging.INFO, - format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", - stream=sys.stdout, - ) - - # Resolve default config paths - agents_config = args.agents_config - if not agents_config: - default_path = os.path.join( - os.path.dirname(os.path.dirname(os.path.dirname(__file__))), - "config", "agents.yaml", - ) - if os.path.exists(default_path): - agents_config = default_path - - log.info("╔══════════════════════════════════════════╗") - log.info("║ Hivemind MCP Server v1.0.0 ║") - log.info("╚══════════════════════════════════════════╝") - log.info("Host: %s:%d", args.host, args.port) - log.info("Agents config: %s", agents_config or "(defaults)") - log.info("Debug: %s", args.debug) - - server = HivemindServer( - agents_config=agents_config, - llm_config=args.llm_config, - ) - - web.run_app(server.app, host=args.host, port=args.port) - - -if __name__ == "__main__": - main() diff --git a/backend/app/mcp/llm_router.py b/backend/app/mcp/llm_router.py deleted file mode 100644 index 700885de..00000000 --- a/backend/app/mcp/llm_router.py +++ /dev/null @@ -1,282 +0,0 @@ -"""Hivemind LLM Router — Route agent requests to appropriate LLM backends. - -Supports three tiers: - 1. Roundtable AI (local, priority 1) - 2. Ollama (local, priority 2) - 3. OpenAI fallback (priority 3) - -API keys are sourced from the Secret Store (AES-256-GCM encrypted), -with env var fallback for development convenience. -""" -from __future__ import annotations - -import json -import logging -import os -from dataclasses import dataclass, field -from typing import Any - -import aiohttp - -from app.secret.store import get_store - -log = logging.getLogger("ucore.mcp.hivemind.llm_router") - - -@dataclass -class LLMBackend: - name: str - base_url: str - api_key: str | None = None - models: list[str] = field(default_factory=list) - priority: int = 1 - type: str = "openai_compatible" - - -class LLMRouter: - """Routes agent requests to the appropriate LLM backend.""" - - def __init__(self, config_path: str | None = None): - self.backends: list[LLMBackend] = [] - self._session: aiohttp.ClientSession | None = None - if config_path: - self._load_config(config_path) - else: - self._init_defaults() - - def _init_defaults(self): - """Initialize with default backend configuration. - - API keys are sourced from the Secret Store (AES-256-GCM encrypted), - with env var fallback for development convenience. - """ - store = get_store() - - self.backends = [ - LLMBackend( - name="roundtable", - base_url=os.environ.get( - "ROUNDTABLE_BASE_URL", "http://localhost:4891/v1" - ), - models=["claude-sonnet-4-20250514", "claude-opus-4-20250514", "gpt-4o"], - priority=1, - ), - LLMBackend( - name="ollama", - base_url=os.environ.get( - "OLLAMA_BASE_URL", "http://localhost:11434/v1" - ), - models=["llama3", "codellama", "mistral"], - priority=2, - ), - LLMBackend( - name="fallback", - base_url="https://api.openai.com/v1", - api_key=store.get("OPENAI_API_KEY") - or os.environ.get("OPENAI_API_KEY"), - models=["gpt-4o-mini"], - priority=3, - type="openai", - ), - ] - - def _resolve_template(self, value: str | None, default: str = "") -> str: - """Resolve ${VAR} templates from env vars or Secret Store.""" - if not value or "${" not in value: - return value if value is not None else default - import re - - store = get_store() - - def _replacer(m: re.Match) -> str: - var_name = m.group(1) - stored = store.get(var_name) - if stored: - return stored - return os.environ.get(var_name, m.group(0)) - - return re.sub(r"\$\{(\w+)\}", _replacer, value) - - def _resolve_api_key(self, value: str | None) -> str | None: - """Resolve ${VAR} templates with Optional[str] return.""" - if not value or "${" not in value: - return value - return self._resolve_template(value) - - def _load_config(self, config_path: str): - """Load backend configuration from a YAML/JSON file. - - Loads backend configuration from a YAML/JSON file. Budgets are read - from budget.yaml by BudgetManager, so no separate wiring is needed. - """ - import yaml - - with open(config_path) as f: - if config_path.endswith((".yaml", ".yml")): - config = yaml.safe_load(f) - else: - config = json.load(f) - - router_config = config.get("router", {}) - - backends_config = router_config.get("backends", {}) - - self.backends = [] - for name, cfg in backends_config.items(): - raw_key = cfg.get("api_key") - self.backends.append( - LLMBackend( - name=name, - base_url=self._resolve_template( - cfg.get("base_url", ""), "" - ), - api_key=self._resolve_api_key(raw_key), - models=cfg.get("models", []), - priority=cfg.get("priority", 99), - type=cfg.get("type", "openai_compatible"), - ) - ) - - # Sort by priority (lower = higher priority) - self.backends.sort(key=lambda b: b.priority) - - async def _get_session(self) -> aiohttp.ClientSession: - """Get or create the HTTP session.""" - if self._session is None or self._session.closed: - self._session = aiohttp.ClientSession() - return self._session - - async def chat_completion( - self, - model: str, - messages: list[dict[str, str]], - system_prompt: str | None = None, - temperature: float = 0.7, - max_tokens: int = 4096, - ) -> dict[str, Any]: - """Send a chat completion request, trying backends in priority order. - - Args: - model: The model name to use (e.g., "claude-sonnet-4-20250514") - messages: List of message dicts with "role" and "content" - system_prompt: Optional system prompt to prepend - temperature: Sampling temperature - max_tokens: Maximum tokens in response - - Returns: - Response dict with "content", "model", "backend" keys - """ - session = await self._get_session() - - # Find backends that support this model, sorted by priority - candidates = [ - b for b in self.backends if model in b.models - ] - if not candidates: - # Try all backends as fallback - candidates = sorted(self.backends, key=lambda b: b.priority) - - errors: list[str] = [] - for backend in candidates: - try: - result = await self._try_backend( - session, backend, model, messages, - system_prompt, temperature, max_tokens, - ) - if result: - return result - except Exception as exc: - err = f"{backend.name}: {exc}" - errors.append(err) - log.warning("Backend %s failed: %s", backend.name, err) - - return { - "error": "All backends failed", - "errors": errors, - "content": None, - } - - async def _try_backend( - self, - session: aiohttp.ClientSession, - backend: LLMBackend, - model: str, - messages: list[dict[str, str]], - system_prompt: str | None, - temperature: float, - max_tokens: int, - ) -> dict[str, Any] | None: - """Try a single backend for chat completion.""" - headers = {"Content-Type": "application/json"} - if backend.api_key: - headers["Authorization"] = f"Bearer {backend.api_key}" - - payload_messages = list(messages) - if system_prompt: - payload_messages.insert(0, { - "role": "system", - "content": system_prompt, - }) - - payload = { - "model": model, - "messages": payload_messages, - "temperature": temperature, - "max_tokens": max_tokens, - } - - url = f"{backend.base_url.rstrip('/')}/chat/completions" - log.debug("Sending request to %s (%s)", backend.name, url) - - async with session.post( - url, json=payload, headers=headers, timeout=aiohttp.ClientTimeout(total=120) - ) as resp: - if resp.status != 200: - text = await resp.text() - log.warning( - "Backend %s returned %d: %s", - backend.name, resp.status, text[:200], - ) - return None - - data = await resp.json() - choices = data.get("choices", []) - if not choices: - return None - - return { - "content": choices[0].get("message", {}).get("content", ""), - "model": data.get("model", model), - "backend": backend.name, - "usage": data.get("usage", {}), - } - - async def health_check(self) -> list[dict[str, Any]]: - """Check health of all configured backends.""" - results = [] - for backend in self.backends: - try: - session = await self._get_session() - url = f"{backend.base_url.rstrip('/')}/models" - async with session.get( - url, timeout=aiohttp.ClientTimeout(total=5) - ) as resp: - results.append({ - "name": backend.name, - "url": backend.base_url, - "healthy": resp.status == 200, - "status": resp.status, - }) - except Exception as exc: - results.append({ - "name": backend.name, - "url": backend.base_url, - "healthy": False, - "error": str(exc), - }) - return results - - async def close(self): - """Close the HTTP session.""" - if self._session and not self._session.closed: - await self._session.close() diff --git a/backend/app/mcp/roundtable_integration.py b/backend/app/mcp/roundtable_integration.py deleted file mode 100644 index 59222a4a..00000000 --- a/backend/app/mcp/roundtable_integration.py +++ /dev/null @@ -1,157 +0,0 @@ -"""Roundtable AI Integration -- Multi-model local agent swarm. - -Roundtable AI provides a local MCP server that wraps Codex CLI, Claude Code CLI, -Cursor, and Gemini CLI as sub-agents. This module integrates it with Hivemind. - -Usage: - roundtable-mcp-server --agents codex,claude,cursor,gemini - -Environment: - CLI_MCP_SUBAGENTS - Comma-separated sub-agents - CLI_MCP_WORKING_DIR - Default working directory -""" -from __future__ import annotations - -import asyncio -import json -import logging -import os -import subprocess -from dataclasses import dataclass -from pathlib import Path -from typing import Any - -log = logging.getLogger("ucore.mcp.hivemind.roundtable") - -ROUNDTABLE_CHECK_FILE = Path.home() / ".roundtable" / "availability_check.json" -ROUNDTABLE_PORT = 4891 - - -@dataclass -class RoundtableAgent: - """A sub-agent discovered by Roundtable AI.""" - - name: str - available: bool - version: str | None = None - model: str | None = None - - -class RoundtableIntegration: - """Integration layer between Hivemind and Roundtable AI.""" - - def __init__(self, agents: list[str] | None = None): - self.agents = agents or ["codex", "claude", "cursor", "gemini"] - self._process: subprocess.Popen | None = None - self._availability: list[RoundtableAgent] = [] - - async def check_availability(self) -> list[RoundtableAgent]: - """Run availability check -- probes each CLI and caches results.""" - venv_python = self._get_venv_python() - cmd = [ - venv_python, "-m", "roundtable_mcp_server", "--check", - ] - try: - proc = await asyncio.create_subprocess_exec( - *cmd, - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE, - ) - _, stderr = await asyncio.wait_for( - proc.communicate(), timeout=30 - ) - if proc.returncode != 0: - log.warning( - "Roundtable check failed: %s", stderr.decode()[:200] - ) - except (asyncio.TimeoutError, FileNotFoundError) as exc: - log.warning("Roundtable check error: %s", exc) - - return self._load_availability() - - def _get_venv_python(self) -> str: - """Resolve python from backend venv or system.""" - project_root = Path(__file__).resolve().parent.parent.parent - venv = project_root / ".venv" / "bin" / "python" - if venv.exists(): - return str(venv) - return "python3" - - def _load_availability(self) -> list[RoundtableAgent]: - """Load availability check results from cache.""" - if not ROUNDTABLE_CHECK_FILE.exists(): - log.info( - "No Roundtable availability cache at %s", - ROUNDTABLE_CHECK_FILE, - ) - return [] - - try: - data = json.loads(ROUNDTABLE_CHECK_FILE.read_text()) - agents = [] - for cli_name, info in data.items(): - agents.append(RoundtableAgent( - name=cli_name, - available=info.get("available", False), - version=info.get("version"), - model=info.get("model"), - )) - self._availability = agents - log.info( - "Roundtable agents: %d available", - sum(1 for a in agents if a.available), - ) - return agents - except (json.JSONDecodeError, KeyError) as exc: - log.warning("Failed to parse Roundtable cache: %s", exc) - return [] - - def get_available_agents(self) -> list[RoundtableAgent]: - """Get cached agent availability (non-blocking).""" - if not self._availability: - return self._load_availability() - return self._availability - - def get_mcp_server_command(self) -> list[str]: - """Get command to start Roundtable as an MCP server.""" - venv_python = self._get_venv_python() - agents_str = ",".join(self.agents) - return [ - venv_python, "-m", "roundtable_mcp_server", - "--agents", agents_str, - ] - - def get_env_config(self) -> dict[str, str]: - """Get environment variables for Roundtable MCP server.""" - return { - "CLI_MCP_SUBAGENTS": ",".join(self.agents), - "CLI_MCP_WORKING_DIR": os.getcwd(), - "ROUNDTABLE_BASE_URL": f"http://localhost:{ROUNDTABLE_PORT}/v1", - } - - def summary(self) -> dict[str, Any]: - """Return a summary dict for health/status endpoints.""" - agents = self.get_available_agents() - available = [a.name for a in agents if a.available] - unavailable = [a.name for a in agents if not a.available] - return { - "installed": True, - "version": "0.8.0", - "mcp_server": "roundtable-mcp-server", - "port": ROUNDTABLE_PORT, - "requested_agents": self.agents, - "available": available, - "unavailable": unavailable, - "env": self.get_env_config(), - } - - -# Singleton -_integration: RoundtableIntegration | None = None - - -def get_roundtable() -> RoundtableIntegration: - global _integration - if _integration is None: - _integration = RoundtableIntegration() - return _integration diff --git a/backend/app/mcp/toon/__init__.py b/backend/app/mcp/toon/__init__.py deleted file mode 100644 index 2d1bb34f..00000000 --- a/backend/app/mcp/toon/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .server import TOONContextServer - -__all__ = ['TOONContextServer'] diff --git a/backend/app/menu/unified_menu_simple.py b/backend/app/menu/unified_menu_simple.py index 0bd19780..5af16d3b 100644 --- a/backend/app/menu/unified_menu_simple.py +++ b/backend/app/menu/unified_menu_simple.py @@ -90,8 +90,6 @@ EXTENSION_LINKS = { "snackmachine-extension": "http://localhost:5175/snackbar?tab=snacks", - "hivemind": "http://localhost:5175/assistui", - "roundtable": "http://localhost:5175/assistui", } log_dir = UDOS_HOME / "logs" diff --git a/backend/app/services/control_service.py b/backend/app/services/control_service.py index 783cb620..60b825cd 100644 --- a/backend/app/services/control_service.py +++ b/backend/app/services/control_service.py @@ -1,9 +1,9 @@ """Control Service — aggregates all uCore ecosystem status into one payload. Used by the Control Panel (Developer Surface) to display: -- Status badges (OpenRouter, Hivemind, Roundtable, Ollama, Feed, Slate, Budget) +- Status badges (OpenRouter, Ollama, Feed, Slate, Budget) - Live feed stream -- Agent status (Hivemind consensus, Roundtable swarm, Ollama models) +- Local model status - Cost dashboard (daily/weekly/monthly) - Active mission (from .tasker) - Tasker overview, MCP servers, Slates, Alerts @@ -14,9 +14,7 @@ import json import logging import os -import socket import subprocess -import sys from datetime import datetime, timezone from pathlib import Path @@ -78,34 +76,6 @@ def _tasker_dir() -> Path | None: return None -def _backend_root() -> Path: - """Resolve backend root directory for detached process starts.""" - return Path(__file__).resolve().parents[2] - - -def _is_port_open(host: str, port: int, timeout: float = 0.4) -> bool: - """Fast TCP check used as a fallback for health endpoints.""" - try: - with socket.create_connection((host, port), timeout=timeout): - return True - except OSError: - return False - - -async def _start_hivemind_server() -> tuple[bool, str]: - """Start Hivemind server on port 8490 if not already running.""" - from app.mcp.hivemind_launcher import start_hivemind - - if _is_port_open("localhost", 8490): - return True, "Hivemind already listening on port 8490" - - try: - start_hivemind() - return True, "Started Hivemind on port 8490" - except Exception as exc: - return False, f"Failed to start Hivemind: {exc}" - - # --------------------------------------------------------------------------- # Status badge checks # --------------------------------------------------------------------------- @@ -163,32 +133,6 @@ async def check_openrouter() -> dict: return result -async def check_hivemind() -> dict: - """Check Hivemind health on port 8490.""" - data = await _http_get("http://localhost:8490/health", timeout=1.0) - if data: - return {"online": True, "detail": "Hivemind responding", "data": data} - if _is_port_open("localhost", 8490): - return {"online": True, "detail": "Hivemind port open (health endpoint pending)"} - return {"online": False, "detail": "Hivemind not reachable on port 8490"} - - -async def check_roundtable() -> dict: - """Check Roundtable agent status.""" - data = await _http_get("http://localhost:8490/api/hivemind/roundtable", timeout=1.0) - if data: - return {"online": True, "detail": "Roundtable available", "data": data} - health = await _http_get("http://localhost:8490/health", timeout=1.0) - if health: - rt = health.get("roundtable") if isinstance(health, dict) else None - return { - "online": True, - "detail": f"Roundtable via Hivemind ({rt.get('status', 'ready')})" if isinstance(rt, dict) else "Roundtable served by Hivemind", - "data": rt if isinstance(rt, dict) else {}, - } - return {"online": False, "detail": "Roundtable not reachable"} - - async def check_ollama() -> dict: """Check Ollama status.""" data = await _http_get("http://localhost:11434/api/tags", timeout=1.5) @@ -220,17 +164,7 @@ async def check_slate() -> dict: async def get_cost_status() -> dict: """Get budget/cost status.""" - data = await _http_get("http://localhost:8490/api/hivemind/llm/cost", timeout=1.0) - if data: - daily = data.get("daily", {}) - return { - "online": True, - "detail": f"${daily.get('used', 0):.2f} / ${daily.get('limit', 0):.2f}", - "daily": daily, - "weekly": data.get("weekly", {}), - "monthly": data.get("monthly", {}), - } - # Fallback: try budget API + # Budget API is the sole cost authority. data = await _http_get("http://localhost:8484/api/budget/status", timeout=1.0) if data: return {"online": True, "detail": "Budget data available", **data} @@ -243,14 +177,6 @@ async def recover_offline_services() -> dict: statuses_before = await _gather_statuses() actions: list[str] = [] - hive_before = statuses_before.get("hivemind", {}) - if not hive_before.get("online"): - ok, detail = await _start_hivemind_server() - actions.append(detail) - if ok: - # Roundtable is hosted in the same server process. - actions.append("Roundtable piggybacks on Hivemind process") - openrouter_before = statuses_before.get("openrouter", {}) if not openrouter_before.get("online"): actions.append("OpenRouter remains unavailable; verify OPENROUTER_API_KEY in Secret Store") @@ -302,25 +228,6 @@ async def get_unprocessed_count() -> int: # Agent status # --------------------------------------------------------------------------- -async def get_hivemind_status() -> dict: - """Get Hivemind consensus status.""" - data = await _http_get("http://localhost:8490/api/hivemind/status", timeout=1.0) - if data: - return data - health = await _http_get("http://localhost:8490/health", timeout=1.0) - if health: - return {"status": "running", "detail": "Hivemind server alive", "data": health} - return {"status": "offline", "detail": "Hivemind not reachable"} - - -async def get_roundtable_status() -> dict: - """Get Roundtable swarm status.""" - data = await _http_get("http://localhost:8490/api/hivemind/roundtable", timeout=1.0) - if data: - return data - return {"status": "unknown", "detail": "Roundtable status unavailable"} - - async def get_ollama_status() -> dict: """Get detailed Ollama status.""" data = await _http_get("http://localhost:11434/api/tags", timeout=1.5) @@ -340,15 +247,7 @@ async def get_ollama_status() -> dict: async def get_cost_summary() -> dict: """Get cost summary (daily, weekly, monthly, top models).""" - data = await _http_get("http://localhost:8490/api/hivemind/llm/cost", timeout=1.0) - if data: - return { - "daily": data.get("daily", {}), - "weekly": data.get("weekly", {}), - "monthly": data.get("monthly", {}), - "top_models": data.get("top_models", []), - } - # Fallback + # Budget API is the sole cost authority. budget = await _http_get("http://localhost:8484/api/budget/status", timeout=1.0) if budget: return {"daily": budget, "weekly": {}, "monthly": {}, "top_models": []} @@ -483,15 +382,6 @@ async def get_active_alerts() -> list[dict]: "source": "feed", }) - # Hivemind - hive = await check_hivemind() - if not hive.get("online"): - alerts.append({ - "type": "error", - "message": "Hivemind not responding on port 8490", - "source": "hivemind", - }) - # OpenRouter credits or_status = await check_openrouter() credits = or_status.get("credits", 0) @@ -539,14 +429,12 @@ async def get_control_status() -> dict: async def _gather_statuses() -> dict: results = await asyncio.gather( check_openrouter(), - check_hivemind(), - check_roundtable(), check_ollama(), check_feed(), check_slate(), get_cost_status(), ) - keys = ["openrouter", "hivemind", "roundtable", "ollama", "feed", "slate", "budget"] + keys = ["openrouter", "ollama", "feed", "slate", "budget"] return dict(zip(keys, results)) @@ -559,9 +447,4 @@ async def _gather_feed() -> dict: async def _gather_agents() -> dict: - hive, rt, ollama = await asyncio.gather( - get_hivemind_status(), - get_roundtable_status(), - get_ollama_status(), - ) - return {"hivemind": hive, "roundtable": rt, "ollama": ollama} + return {"ollama": await get_ollama_status()} diff --git a/backend/app/mcp/feed/feed_server.py b/backend/app/services/feed_store.py similarity index 91% rename from backend/app/mcp/feed/feed_server.py rename to backend/app/services/feed_store.py index abedcdda..b217a521 100644 --- a/backend/app/mcp/feed/feed_server.py +++ b/backend/app/services/feed_store.py @@ -1,6 +1,6 @@ -"""feed_server — MCP tools for the Feed Activity Pod (SQLite). +"""Feed activity store for the Activity Pod (SQLite). -Exposes four tools via the MCP dispatch system: +Provides four internal operations: - feed_ingest_activity: insert an activity event into user_activity - feed_query: query activities by source, timeframe, importance - feed_suggest_binders: AI-driven binder suggestions from activity clusters @@ -19,7 +19,7 @@ from app.core.settings import settings -log = logging.getLogger("ucore.mcp.feed_server") +log = logging.getLogger("ucore.services.feed_store") DEFAULT_POD_PATH = settings.udos_home / "pods" / "activity.db" HERE = Path(__file__).resolve().parent @@ -27,7 +27,7 @@ class FeedServer: - """MCP server for Feed Activity Pod ingestion, query, suggestion, and linking.""" + """Store for Feed Activity Pod ingestion, query, suggestion, and linking.""" def __init__(self, pod_path: str | None = None): self._pod_path = Path(pod_path or DEFAULT_POD_PATH).expanduser() @@ -51,7 +51,7 @@ def _ensure_schema(self) -> None: def close(self) -> None: self._conn.close() - # ── MCP Tool: feed_ingest_activity ──────────────────────────── + # ── Activity ingestion ──────────────────────────────────────── async def ingest_activity( self, @@ -87,7 +87,7 @@ async def ingest_activity( ) return {"id": row_id, "message": "Activity ingested"} - # ── MCP Tool: feed_query ────────────────────────────────────── + # ── Activity query ──────────────────────────────────────────── async def query_feed( self, @@ -115,7 +115,7 @@ async def query_feed( rows = cursor.fetchall() return [dict(row) for row in rows] - # ── MCP Tool: feed_suggest_binders ──────────────────────────── + # ── Binder suggestions ──────────────────────────────────────── async def suggest_binders( self, min_confidence: float = 0.5, @@ -171,7 +171,7 @@ async def suggest_binders( return suggestions - # ── MCP Tool: feed_link_task ─────────────────────────────────── + # ── Task links ──────────────────────────────────────────────── async def link_task_to_activity( self, diff --git a/backend/app/services/provider_router.py b/backend/app/services/provider_router.py index 9463c581..623b155d 100644 --- a/backend/app/services/provider_router.py +++ b/backend/app/services/provider_router.py @@ -46,7 +46,7 @@ def _load_env_file(path: Path) -> None: # Load env files on import _env_paths = [ settings.udos_home / ".env", - settings.config_dir / "hivemind.env", + settings.config_dir / "providers.env", ] for _ep in _env_paths: _load_env_file(_ep) diff --git a/backend/app/mcp/toon/server.py b/backend/app/services/toon_context.py similarity index 98% rename from backend/app/mcp/toon/server.py rename to backend/app/services/toon_context.py index a83ec8ee..4b7fa6d9 100644 --- a/backend/app/mcp/toon/server.py +++ b/backend/app/services/toon_context.py @@ -11,7 +11,7 @@ from app.core.settings import settings from app.utils.sqlite_helpers import get_sqlite_connection -log = logging.getLogger("ucore.mcp.toon") +log = logging.getLogger("ucore.services.toon_context") def default_toon_db_path() -> Path: @@ -38,8 +38,8 @@ class TOONContextEntry: class TOONContextServer: - """TOON Context Server - Token-Optimized Object Notation for AI context. - + """TOON context cache for Token-Optimized Object Notation. + Converts structured data (JSON, CSV, Markdown) to TOON format which is 30-60% more token-efficient while preserving semantic meaning. """ diff --git a/backend/app/skills/builtin/skill_nuggets_and_spool.py b/backend/app/skills/builtin/skill_nuggets_and_spool.py index 61a90646..1809bfc2 100644 --- a/backend/app/skills/builtin/skill_nuggets_and_spool.py +++ b/backend/app/skills/builtin/skill_nuggets_and_spool.py @@ -240,7 +240,7 @@ class SpoolArchiveSkill(BaseSkill): SkillParam( name="component_type", type="string", - description="Type: skill, snack, mcp, hivemind, secret, css, legacy", + description="Type: skill, snack, secret, css, legacy", default="legacy", ), SkillParam( @@ -397,7 +397,7 @@ class SpoolBackupSkill(BaseSkill): SkillParam( name="component_type", type="string", - description="Type: skill, snack, mcp, hivemind, secret, css, legacy", + description="Type: skill, snack, secret, css, legacy", default="legacy", ), SkillParam( @@ -559,7 +559,7 @@ class SpoolDestroySkill(BaseSkill): SkillParam( name="component_type", type="string", - description="Type: skill, snack, mcp, hivemind, secret, css", + description="Type: skill, snack, secret, css", default="skill", ), SkillParam( @@ -994,7 +994,7 @@ class SpoolListSkill(BaseSkill): SkillParam( name="component_type", type="string", - description="Filter by type: skill, snack, mcp, hivemind, secret, css, legacy", + description="Filter by type: skill, snack, secret, css, legacy", default="", ), SkillParam( @@ -1487,7 +1487,7 @@ class NuggetCreateSkill(BaseSkill): SkillParam( name="component_type", type="string", - description="Type: skill, snack, mcp, hivemind, secret, css, legacy", + description="Type: skill, snack, secret, css, legacy", default="legacy", ), SkillParam( @@ -1648,7 +1648,7 @@ class NuggetListSkill(BaseSkill): SkillParam( name="component_type", type="string", - description="Filter by type: skill, snack, mcp, hivemind, secret, css, legacy", + description="Filter by type: skill, snack, secret, css, legacy", default="", ), SkillParam( @@ -1919,7 +1919,7 @@ class NuggetDiscoverSkill(BaseSkill): SkillParam( name="component_type", type="string", - description="Filter by type: skill, snack, mcp, hivemind, secret, css, legacy", + description="Filter by type: skill, snack, secret, css, legacy", default="", ), SkillParam( diff --git a/backend/app/utils/config_loader.py b/backend/app/utils/config_loader.py index 250afade..8e62a19c 100644 --- a/backend/app/utils/config_loader.py +++ b/backend/app/utils/config_loader.py @@ -57,24 +57,6 @@ def _read_yaml(filename: str) -> dict[str, Any] | None: "health": {"path": "/api/health"}, "description": "Container orchestrator and workflow runner", }, - { - "id": "hivemind", - "name": "Hivemind", - "category": "system", - "host": "localhost", - "port": 8490, - "health": {"path": "/health"}, - "description": "AI agent routing gateway", - }, - { - "id": "vault-mcp", - "name": "Vault MCP", - "category": "user", - "host": "localhost", - "port": 8765, - "health": {"path": "/health"}, - "description": "Vault MCP bridge", - }, { "id": "ollama", "name": "Ollama", diff --git a/backend/config/llm_router.yaml b/backend/config/llm_router.yaml deleted file mode 100644 index 08c2b9b6..00000000 --- a/backend/config/llm_router.yaml +++ /dev/null @@ -1,84 +0,0 @@ -# LLM Router Configuration — maps agents.yaml models to backend endpoints -# Used by: HivemindServer → LLMRouter → CostManager -# -# Priority: lower number = tried first -# API keys sourced in order: Secret Store (AES-256-GCM) → env var → this file -# -# Cost tiers: -# free: $0/1K tokens (Ollama local models) -# cheap: $0.00015–0.002 (gpt-4o-mini, deepseek-v4-flash, qwen3.6) -# premium: $0.0025+ (claude-opus-4.7, glm-5.1, gpt-4o) - -router: - # ─── Budget ──────────────────────────────────────────────────── - budgets: - daily: 2.00 - weekly: 10.00 - monthly: 30.00 - - # ─── Routing Rules — determines which backend receives a task ── - routing_rules: - # Default: everything goes to Ollama (free, local) - default: - provider: ollama - model: qwen2.5-coder:3b - reason: "Default local-first routing — zero cost, zero latency" - - # Complexity override: tasks scoring >7 get premium cloud - complexity_threshold: - score: 7 - provider: openrouter - model: claude-opus-4.7 - reason: "High-complexity task — routed to premium cloud model" - fallback: ollama # if OpenRouter is down, fall back - fallback_model: qwen2.5-coder:7b-instruct-q4_K_M - - # Budget exhausted override: all traffic to Ollama - budget_exhausted: - provider: ollama - model: qwen2.5-coder:3b - reason: "Budget limit reached — free local model only" - - # ─── Cost Tiers ──────────────────────────────────────────────── - cost_tiers: - free: [qwen2.5-coder:3b, llama3, codellama, mistral] - cheap: [deepseek-v4-flash, qwen3.6-27b, gpt-4o-mini] - premium: [glm-5.1, claude-opus-4.7, gpt-4o] - - # ─── Backends ────────────────────────────────────────────────── - backends: - # ─── Ollama — default, free, local ────────────────────────── - ollama: - base_url: "http://localhost:11434/v1" - api_key: null - models: - - qwen2.5-coder:3b # dev agent, gridsmith-dev agent (free) - - qwen2.5-coder:7b-instruct-q4_K_M # larger coder (free) - - codegemma:2b # f/e tasks (free) - - codellama:7b # general coding (free) - - mistral:7b # general purpose (free) - - qwen2.5-coder:0.5b # tiny fallback (free) - priority: 1 - type: openai_compatible - - # ─── OpenRouter — premium fallback for complex tasks ───────── - openrouter: - base_url: "https://openrouter.ai/api/v1" - api_key: "${OPENROUTER_API_KEY}" - models: - - glm-5.1 # architecture agent (premium) - - claude-opus-4.7 # reviewer agent (premium) - - deepseek-v4-flash # debugger agent (cheap) - - qwen3.6-27b # docgen agent (cheap) - priority: 2 - type: openai_compatible - - # ─── OpenAI Fallback — last resort ──────────────────────────── - fallback: - base_url: "https://api.openai.com/v1" - api_key: "${OPENAI_API_KEY}" - models: - - gpt-4o-mini # cheap - - gpt-4o # premium - priority: 3 - type: openai diff --git a/backend/tests/test_toon_server.py b/backend/tests/test_toon_server.py index 14ccd14c..e87da277 100644 --- a/backend/tests/test_toon_server.py +++ b/backend/tests/test_toon_server.py @@ -1,6 +1,6 @@ from __future__ import annotations -from app.mcp.toon.server import TOONContextServer +from app.services.toon_context import TOONContextServer def test_toon_server_basic(): diff --git a/docs/FEATURE_SPEC_ASSISTUI_DEVELOPER_CHAT_LANE_SEPARATION.md b/docs/FEATURE_SPEC_ASSISTUI_DEVELOPER_CHAT_LANE_SEPARATION.md index 2fbf9eb3..e8eae5f2 100644 --- a/docs/FEATURE_SPEC_ASSISTUI_DEVELOPER_CHAT_LANE_SEPARATION.md +++ b/docs/FEATURE_SPEC_ASSISTUI_DEVELOPER_CHAT_LANE_SEPARATION.md @@ -59,7 +59,7 @@ Focuses on task planning with known statuses, priorities, and board types. Lists real dev APIs: - Repos: list, files, review, status, diff, file-preview, stage, commit -- Skills & MCP: list skills, execute skills, MCP tools, diagnostics +- Skills: list and execute governed skills - Health: control status, Ollama status, system info Lane-aware: passes current lane and workspace context. @@ -141,4 +141,4 @@ To add **actual API calling** (not just prompts), the next step is: 2. **Dev Chat Tool Use**: Similarly for `/api/developer/repos/{name}/review` etc. 3. **Streaming with tool results**: SSE stream a `{ "tool_call": { ... } }` event, backend executes, streams `{ "tool_result": { ... } }` back -The system prompts already name every available endpoint. The LLM knows what to call. The missing piece is the execution plumbing. \ No newline at end of file +The system prompts already name every available endpoint. The LLM knows what to call. The missing piece is the execution plumbing. diff --git a/docs/FEED_SYSTEM_SPEC.md b/docs/FEED_SYSTEM_SPEC.md index 2582a971..706356c8 100644 --- a/docs/FEED_SYSTEM_SPEC.md +++ b/docs/FEED_SYSTEM_SPEC.md @@ -29,7 +29,7 @@ User Activity Sources: Browser (Chrome/Arc/Safari) → Email (Mail.app) → Messages (iMessage) Alerts (Push) → Search Queries → Calendar Events → Clipboard History ↓ -Feed Ingest Layer (FeedServer MCP) +Feed Ingest Layer (FeedServer store) - feed_ingest_activity: insert into Activity Pod - feed_query: query by source, timeframe, importance - feed_suggest_binders: AI-driven binder suggestions @@ -65,7 +65,7 @@ Indexes: timestamp, contact_id, source, (source, source_id) ## 5. Feed MCP Server -**Location:** `backend/app/mcp/feed/feed_server.py` +**Location:** `backend/app/services/feed_store.py` **Class:** `FeedServer` Tools: @@ -134,7 +134,7 @@ Source configuration for: ## 11. Implementation Status - [x] Activity Pod schema (5 tables, 4 indexes) -- [x] FeedServer with 4 MCP tools +- [x] FeedServer store with four internal operations - [x] Feed API routes wired into routes.py - [x] FeedConsumer bridging feed → Spool - [x] Frontend Pinia feed store @@ -142,4 +142,4 @@ Source configuration for: - [x] .clinerules updated with Feed System rules - [ ] FeedPanel.vue on Developer Surface (future) - [ ] Nugget runtime scripts (ingest_browser.py, ingest_email.py, etc.) -- [ ] AI-powered binder suggestion engine (keyword-based placeholder in place) \ No newline at end of file +- [ ] AI-powered binder suggestion engine (keyword-based placeholder in place) diff --git a/docs/PLATES_SYSTEM_SPEC.md b/docs/PLATES_SYSTEM_SPEC.md index 48d03097..0a4c0d7d 100644 --- a/docs/PLATES_SYSTEM_SPEC.md +++ b/docs/PLATES_SYSTEM_SPEC.md @@ -35,7 +35,7 @@ Every plate has: plate: id: "skill.recover_port_conflict" # Unique identifier version: "1.0.0" # SemVer - domain: "skill" # skill | snack | mcp | hivemind | secret | css + domain: "skill" # skill | snack | secret | css description: "Detect and resolve port conflicts" schema: # Pydantic/JSON Schema validation type: object @@ -65,7 +65,6 @@ plates/ │ ├── skill_plate.py # Cookiecutter template: skill scaffold │ └── ... ├── snacks/ # Snack plates -├── hivemind/ # Hivemind workflow plates ├── secrets/ # Variable/secret plates └── css/ # CSS/USX theme plates ``` @@ -112,7 +111,7 @@ class PlateMeta(BaseModel): """Canonical plate metadata — extends SkillMeta pattern.""" id: str version: str = Field(pattern=r"^\d+\.\d+\.\d+$") - domain: Literal["skill", "snack", "mcp", "hivemind", "secret", "css"] + domain: Literal["skill", "snack", "secret", "css"] description: str = "" schema: dict[str, Any] = {} source: Literal["builtin", "user", "community"] = "builtin" @@ -277,7 +276,6 @@ plate: 3. On detecting corruption: run DESTROY/REBUILD protocol 4. After successful modification, promote to plate: - `plates/skills/` for new skills (via cookiecutter) - - `plates/mcp/` for new MCP tools - Update version number on changes 5. Never delete a plate — archive with version suffix ``` diff --git a/docs/SPOOL_SPEC.md b/docs/SPOOL_SPEC.md index 50a8de81..5781e4ef 100644 --- a/docs/SPOOL_SPEC.md +++ b/docs/SPOOL_SPEC.md @@ -8,7 +8,7 @@ **Component:** uCore backend + brain_sync **Purpose:** A unified, searchable log of all uCore ecosystem activity — skill executions, system events, vault operations, clipboard events — exposed as a spool feed for -the clipboard popover Logs tab, brain_sync synthesis, and MCP tools. +the clipboard popover Logs tab and brain_sync synthesis. --- @@ -21,7 +21,7 @@ entries into a queryable event stream. |---------|----------------| | **Feed source** | `~/.ucore/logs/*.log` (append-only log files) | | **Parser** | `backend/app/services/spool_reader.py` | -| **Feed viewer** | MCP tools + clipboard popover Logs tab (future) | +| **Feed viewer** | Clipboard popover Logs tab (future) | | **Real-time updates** | File watcher (future, via IPC) | | **Search & filter** | By level, module, date, full-text | | **Actionable** | Log entries link to skill re-execution (future) | diff --git a/scripts/demo_optimized_workflow.py b/scripts/demo_optimized_workflow.py index faa39ac5..d67ef453 100755 --- a/scripts/demo_optimized_workflow.py +++ b/scripts/demo_optimized_workflow.py @@ -6,7 +6,6 @@ """ import asyncio -import json import sys from pathlib import Path @@ -22,7 +21,7 @@ async def demo_toon_optimization(): print("\n=== TOON Context Optimization Demo ===\n") # Import TOON server - from app.mcp.toon.server import TOONContextServer + from app.services.toon_context import TOONContextServer # Create server toon_server = TOONContextServer() @@ -106,7 +105,7 @@ async def demo_flow_router(): # Get analytics analytics = router.get_analytics() - print(f"Flow Router Analytics:") + print("Flow Router Analytics:") print(f" Total requests: {analytics['total_requests']}") print(f" Cost savings: ${analytics['cost_savings']:.4f}") print(f" Token savings: {analytics['token_savings']} tokens") @@ -114,7 +113,7 @@ async def demo_flow_router(): # Get history history = router.get_routing_history(limit=3) - print(f"\nRecent routing history:") + print("\nRecent routing history:") for i, entry in enumerate(history): print(f" {i+1}. {entry['task_description'][:50]}... → {entry['selected_provider']}/{entry['selected_model']}") diff --git a/scripts/ucore_watchdog.sh b/scripts/ucore_watchdog.sh index 47f29ea5..52b46212 100755 --- a/scripts/ucore_watchdog.sh +++ b/scripts/ucore_watchdog.sh @@ -1,5 +1,5 @@ #!/bin/bash -# uCore Watchdog — health/self-heal loop for backend + orchestration services. +# uCore Watchdog — health loop for the backend and frontend. # Intended to run via launchd StartInterval. set -euo pipefail @@ -25,10 +25,6 @@ check_backend() { curl -s --max-time 3 "http://127.0.0.1:8484/api/health" > /dev/null 2>&1 } -check_hivemind() { - curl -s --max-time 2 "http://127.0.0.1:8490/health" > /dev/null 2>&1 -} - check_vite() { curl -s --max-time 3 "http://127.0.0.1:5175" > /dev/null 2>&1 } @@ -225,11 +221,6 @@ main() { fi fi - if check_backend && ! check_hivemind; then - log "Hivemind health failed; attempting control recovery" - attempt_control_recover || true - fi - if check_backend; then log "Triggering autonomy health action" run_autonomy_health_action