From b0f70fd2beb60acd6e205cf8b22f08a81cc5e213 Mon Sep 17 00:00:00 2001 From: Vicky Wonder Date: Thu, 7 May 2026 17:27:46 -0700 Subject: [PATCH] feat: make Conductor use native Swarm fallback Treat Workspace-native Swarm as the official Conductor fallback when the dashboard mission API is unavailable. Preserve dashboard-first dispatch, native status/cancel handling, provider-neutral setup docs, and regression coverage for gateway capability detection, swarm health, roster/profile handling, and native Conductor responses. --- .env.example | 7 +- README.md | 20 +- docs/troubleshooting.md | 14 +- src/components/swarm/swarm-health-strip.tsx | 40 +- src/hooks/use-crew-status.ts | 1 + src/routeTree.gen.ts | 21 + src/routes/api/-conductor-spawn.test.ts | 69 ++++ src/routes/api/-swarm-dispatch.test.ts | 58 +++ src/routes/api/-swarm-health.test.ts | 62 +++ src/routes/api/conductor-spawn.ts | 218 ++++++++++- src/routes/api/conductor-stop.ts | 66 +++- src/routes/api/crew-status.ts | 42 +- src/routes/api/swarm-dispatch.ts | 360 +++++++++++------- src/routes/api/swarm-health.ts | 220 ++++++++--- src/routes/api/swarm-missions.ts | 98 ++++- src/routes/api/swarm-runtime.ts | 53 +-- .../gateway/hooks/use-conductor-gateway.ts | 13 +- .../__tests__/gateway-capabilities.test.ts | 112 ++++++ src/server/gateway-capabilities.ts | 20 +- src/server/swarm-environment.ts | 6 +- src/server/swarm-memory.ts | 13 +- src/server/swarm-missions.test.ts | 72 ++++ src/server/swarm-missions.ts | 96 ++++- src/server/swarm-profile-config.test.ts | 33 +- src/server/swarm-profile-config.ts | 139 ++++++- src/server/swarm-roster.test.ts | 16 + src/server/swarm-roster.ts | 27 ++ swarm.yaml | 8 +- 28 files changed, 1612 insertions(+), 292 deletions(-) create mode 100644 src/routes/api/-conductor-spawn.test.ts create mode 100644 src/routes/api/-swarm-health.test.ts create mode 100644 src/server/swarm-roster.test.ts diff --git a/.env.example b/.env.example index 645408bcd3..7c5bc47022 100644 --- a/.env.example +++ b/.env.example @@ -10,7 +10,7 @@ # container needs the key for whichever provider you configured in # ~/.hermes/config.yaml. Common options: # -# Anthropic (Claude): https://console.anthropic.com/settings/keys +# OpenAI Codex / OpenAI-compatible: configure through `hermes setup` / `hermes model` # OpenAI (GPT / o-series): https://platform.openai.com/api-keys # OpenRouter (many models, free tier available): https://openrouter.ai/keys # Google (Gemini): https://aistudio.google.com/app/apikey @@ -18,7 +18,6 @@ # # Uncomment ONLY the key(s) for the providers you actually use. -# ANTHROPIC_API_KEY=sk-ant-... # OPENAI_API_KEY=sk-... # OPENROUTER_API_KEY=sk-or-v1-... # GOOGLE_API_KEY=AIza... @@ -51,8 +50,8 @@ # Set this if hermes-agent is installed elsewhere # HERMES_AGENT_PATH=/path/to/hermes-agent -# Server port (default: 3002) -# PORT=3002 +# Server port (default: 3000) +# PORT=3000 # ══════════════════════════════════════════════════════════════ # Security diff --git a/README.md b/README.md index 5381169666..9283b076d0 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ > Not a chat wrapper. A complete workspace — orchestrate agents, browse memory, manage skills, and control everything from one interface. -> **v2 — zero-fork.** Clone, don't fork. Runs on vanilla [`NousResearch/hermes-agent`](https://github.com/NousResearch/hermes-agent) installed via Nous's own installer. Chat, sessions, memory, skills, jobs, MCP, terminal, dashboard, Agent View, and Operations are all in vanilla parity. **Conductor** currently requires an additional dashboard plugin not in upstream yet — the UI shows a clear placeholder when that endpoint isn't available ([#262](https://github.com/outsourc-e/hermes-workspace/issues/262)). Everything else works with zero patches. +> **v2 — zero-fork.** Clone, don't fork. Runs on vanilla [`NousResearch/hermes-agent`](https://github.com/NousResearch/hermes-agent) installed via Nous's own installer. Chat, sessions, memory, skills, jobs, MCP, terminal, dashboard, Agent View, and Operations are all in vanilla parity. **Conductor** uses the dashboard mission API when available and falls back to Workspace-native Swarm dispatch (`mode: native-swarm`) when the dashboard endpoint is absent, preserving zero-fork behavior ([#262](https://github.com/outsourc-e/hermes-workspace/issues/262)). ![Hermes Workspace](./docs/screenshots/splash.png) @@ -48,7 +48,7 @@ Start here: [docs/swarm/](./docs/swarm/) - 🔌 **MCP** — Full /mcp page (catalog + marketplace + sources), or fallback to local config CRUD - 📁 **Files + Terminal** — Full workspace file browser with Monaco; cross-platform PTY terminal - 🎮 **Operations** — Multi-agent dashboard with profile presets (Sage/Trader/Builder/Scribe/Ops) and 'Needs setup' detection -- 📡 **Conductor** — Mission dispatch + decomposition (requires upstream dashboard plugin, see [#262](https://github.com/outsourc-e/hermes-workspace/issues/262)) +- 📡 **Conductor** — Mission dispatch + decomposition with dashboard-backed missions when available and Workspace-native Swarm fallback otherwise - 👥 **Agent View** — Live agent panel in chat with avatar, queue, history, usage meter - 🐝 **Swarm Mode** — Persistent tmux-backed Hermes Agent workers with role-based dispatch - 🗄️ **Dashboard** — Aggregated overview: sessions, model mix, cost ledger, attention card, ops strip @@ -213,8 +213,7 @@ HERMES_API_URL=http://127.0.0.1:8642 # Optional: provider keys the Hermes Agent gateway can read at runtime. # You only need the key(s) for whichever provider(s) you actually use. -# ANTHROPIC_API_KEY=*** # Anthropic -# OPENAI_API_KEY=sk-... # GPT / o-series +# OPENAI_API_KEY=sk-... # GPT / o-series / OpenAI-compatible # OPENROUTER_API_KEY=sk-or-v1-... # OpenRouter (incl. free models) # GOOGLE_API_KEY=AIza... # Gemini # (Ollama / LM Studio / local servers don't need a key) @@ -320,7 +319,7 @@ The Docker setup runs both the **Hermes Agent gateway** and **Hermes Workspace** - **Docker** - **Docker Compose** -- **Anthropic API Key** — [Get one here](https://console.anthropic.com/settings/keys) (required for the agent gateway) +- **A configured Hermes Agent model provider** — run `hermes setup` / `hermes model`, or provide a key for whichever provider you use. This workspace does not require Anthropic. ### Step 1: Configure Environment @@ -334,8 +333,7 @@ Edit `.env` and add **at least one** LLM provider key — whichever provider you ```env # Pick one (or more). You do NOT need all of these. -# ANTHROPIC_API_KEY=*** # Anthropic -# OPENAI_API_KEY=sk-... # GPT / o-series +# OPENAI_API_KEY=sk-... # GPT / o-series / OpenAI-compatible # OPENROUTER_API_KEY=sk-or-v1-... # OpenRouter (free models available) # GOOGLE_API_KEY=AIza... # Gemini ``` @@ -586,7 +584,7 @@ Verify: `curl http://localhost:8642/health` should return `{"status": "ok"}`. v2+ runs on vanilla `hermes-agent`. **No fork required.** The upstream ships every endpoint the workspace needs for chat, sessions, memory, skills, config, jobs, MCP, terminal, and Agent View. -**One known exception:** **Conductor** uses a dashboard plugin that hasn't landed upstream yet. When the workspace detects the missing endpoint, the Conductor screen shows a clear "Upstream not ready" placeholder with a link to [issue #262](https://github.com/outsourc-e/hermes-workspace/issues/262) instead of failing mid-action. Everything else works. +**Conductor note:** when the dashboard mission API is available, Workspace uses it directly. When that endpoint is absent, Workspace uses its native Swarm fallback and returns `mode: native-swarm`. The fallback dispatches through Workspace Swarm workers, keeps status available through `/api/conductor-spawn?missionId=...`, and cancels through `/api/conductor-stop`. If you're pinned to an older `hermes-agent` version and missing core endpoints, the workspace will degrade gracefully to **portable mode** with basic chat — upgrade upstream to restore full features. @@ -598,7 +596,7 @@ If using Docker Compose and getting auth errors: ```bash grep -E '_API_KEY' .env - # Should show one of: ANTHROPIC_API_KEY, OPENAI_API_KEY, OPENROUTER_API_KEY, GOOGLE_API_KEY, ... + # Should show one of: OPENAI_API_KEY, OPENROUTER_API_KEY, GOOGLE_API_KEY, or another provider key you intentionally use. ``` (hermes-agent reads whichever key matches the provider configured in `~/.hermes/config.yaml`.) @@ -661,13 +659,13 @@ The Docker setup runs both automatically — no action needed if using `docker c | Mobile PWA + Tailscale | Install as native-feeling app on any device | | Themes | Hermes / Nous / Bronze / Slate / Mono (light + dark) | | Capability gates | Graceful 'upstream not ready' placeholders | -| Multi-provider | Anthropic, OpenAI, OpenRouter, Google, Ollama, LM Studio, vLLM, Atomic Chat | +| Multi-provider | OpenAI/OpenAI-compatible, OpenRouter, Google, Ollama, LM Studio, vLLM, Atomic Chat, and other Hermes-supported providers | ### In progress 🔨 | Feature | Status | |---|---| -| Conductor missions | Workspace UI is shipped; awaiting upstream dashboard plugin (see [#262](https://github.com/outsourc-e/hermes-workspace/issues/262)) | +| Conductor missions | Workspace UI is shipped; uses dashboard mission API when available and Workspace-native Swarm fallback otherwise (see [#262](https://github.com/outsourc-e/hermes-workspace/issues/262)) | | Native Desktop App (Electron) | Spec'd; PWA install path works today | ### Coming 🔜 diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index cf6204a6c4..e0a6ac1eaa 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -13,8 +13,8 @@ Common setup issues and how to fix them. **Fix:** ```bash -# Find your claude env file -claude config env-path +# Find your Hermes env file +hermes config env-path # Usually: ~/.hermes/.env # Check for the key @@ -43,7 +43,7 @@ After fixing, restart the gateway: `hermes gateway run --replace` **Checklist (in order):** -1. Is the gateway running? `pgrep -af "claude.*gateway"` +1. Is the gateway running? `hermes gateway status` or `pgrep -af "hermes.*gateway"` 2. Is port 8642 bound? `curl -sf http://127.0.0.1:8642/health` 3. Is Workspace `.env` correct? `grep HERMES_API_URL ~/hermes-workspace/.env` - Should be: `HERMES_API_URL=http://127.0.0.1:8642` @@ -122,10 +122,10 @@ This means the Vite SSR server tried `GET /api/gateway-status` which internally If nothing above helps, run this and share the output: ```bash -echo "=== claude version ===" && claude --version 2>&1 -echo "=== claude env path ===" && claude config env-path 2>&1 -echo "=== claude env (redacted) ===" && grep -E "^(API_SERVER|CLAUDE_)" "$(claude config env-path 2>/dev/null || echo ~/.hermes/.env)" 2>&1 -echo "=== gateway process ===" && pgrep -af "claude.*gateway" 2>&1 || echo "not running" +echo "=== hermes version ===" && hermes --version 2>&1 +echo "=== hermes env path ===" && hermes config env-path 2>&1 +echo "=== hermes env (redacted) ===" && grep -E "^(API_SERVER|HERMES_|CLAUDE_)" "$(hermes config env-path 2>/dev/null || echo ~/.hermes/.env)" 2>&1 +echo "=== gateway process ===" && pgrep -af "hermes.*gateway" 2>&1 || echo "not running" echo "=== port 8642 ===" && (ss -tlnp 2>/dev/null || lsof -iTCP:8642 -sTCP:LISTEN 2>/dev/null) | grep 8642 || echo "not bound" echo "=== health check ===" && curl -sf http://127.0.0.1:8642/health 2>&1 || echo "not reachable" echo "=== workspace .env ===" && grep CLAUDE ~/hermes-workspace/.env 2>&1 || echo "no .env" diff --git a/src/components/swarm/swarm-health-strip.tsx b/src/components/swarm/swarm-health-strip.tsx index 5832d2062d..aff0c01aaa 100644 --- a/src/components/swarm/swarm-health-strip.tsx +++ b/src/components/swarm/swarm-health-strip.tsx @@ -19,8 +19,16 @@ type WorkerHealth = { model: string provider: string recentAuthErrors: number + recentFallbacks: number lastErrorAt: string | null lastErrorMessage: string | null + lastFallbackAt: string | null + lastFallbackMessage: string | null + modelAuthStatus: 'ready' | 'primary-auth-failed' | 'fallback-active' | 'not-configured' | 'unknown' + primaryAuthOk: boolean | null + fallbackActive: boolean + fallbackProvider: string | null + fallbackModel: string | null } type HealthResponse = { @@ -33,6 +41,11 @@ type HealthResponse = { totalWorkers: number wrappersConfigured: number totalAuthErrors24h: number + totalFallbacks24h: number + workersUsingFallback: number + workersPrimaryAuthFailed: number + degraded: boolean + warnings: string[] distinctModels: string[] distinctProviders: string[] } @@ -110,6 +123,9 @@ export function SwarmHealthStrip({ targetWorkerId }: { targetWorkerId?: string | const workspaceModel = data?.workspaceModel ?? '—' const apiUrl = data?.agentApiUrl ?? data?.claudeApiUrl ?? '—' const totalAuthErrors = data?.summary.totalAuthErrors24h ?? 0 + const totalFallbacks = data?.summary.totalFallbacks24h ?? 0 + const degraded = data?.summary.degraded ?? false + const warnings = data?.summary.warnings ?? [] const wrappersConfigured = data?.summary.wrappersConfigured ?? 0 const totalWorkers = data?.summary.totalWorkers ?? 0 const distinctModels = data?.summary.distinctModels ?? [] @@ -120,7 +136,7 @@ export function SwarmHealthStrip({ targetWorkerId }: { targetWorkerId?: string |
- + Swarm health
@@ -148,13 +164,23 @@ export function SwarmHealthStrip({ targetWorkerId }: { targetWorkerId?: string |
+ {degraded ? ( +
+
Primary model readiness degraded.
+
+ Auth errors: {totalAuthErrors}. Fallbacks: {totalFallbacks}. Reply smoke tests can pass on fallback; fix primary auth before production swarm work. +
+ {warnings.length > 0 ?
{warnings.join(' ')}
: null} +
+ ) : null} +
Gateway: {apiUrl} {distinctModels.length > 0 ? ( @@ -164,8 +190,8 @@ export function SwarmHealthStrip({ targetWorkerId }: { targetWorkerId?: string |
- Smoke test: dispatch a tiny prompt to{' '} - {pingTarget ?? 'no worker'} and confirm a real reply. + Reply smoke test: dispatch a tiny prompt to{' '} + {pingTarget ?? 'no worker'}. This confirms a reply, not primary-model readiness.