plans: hermes agent uses OpenRouter (default deepseek-v4-flash), document off-the-shelf rejection
Swap anthropic-sdk-go for openai-go against the OpenRouter API; default model deepseek/deepseek-v4-flash with Exacto routing and ZDR provider pinning. Record why Nous Hermes Agent (and hosted MCP connectors) were rejected for the resident role. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,25 +27,54 @@ But the hard plumbing already exists and is reused as-is:
|
||||
- `agent_activity` hypertable + `correlation_id` conventions — where tool
|
||||
calls get logged so the control room can show the agent working.
|
||||
|
||||
## Architecture decision: hand-rolled tool loop, not the MCP connector
|
||||
## Architecture decisions
|
||||
|
||||
Two ways to let Claude drive the tools:
|
||||
### Hand-rolled tool loop, not a hosted connector or off-the-shelf agent
|
||||
|
||||
1. **Anthropic MCP connector** (`mcp_servers` param on the Messages API):
|
||||
zero loop code, but Anthropic's servers must reach the MCP endpoint over
|
||||
Three ways to get an LLM driving the tools; decision is (3):
|
||||
|
||||
1. **Hosted MCP connector** (e.g. Anthropic's `mcp_servers` param): zero
|
||||
loop code, but the provider's servers must reach the MCP endpoint over
|
||||
the public internet. `mcp.hubris.network` is currently exposed *without
|
||||
auth* — a hole the [gaps plan](2026-07-08-oikos-gaps-and-improvements.md)
|
||||
says to close, not to build on. Keeping the MCP surface mesh-private is
|
||||
the right posture for a homelab control plane.
|
||||
2. **Hand-rolled agent loop** (~200 lines): Hermes calls the Messages API
|
||||
with tool definitions translated from `tools/list`, executes Claude's
|
||||
tool_use blocks through its *existing* `mcpClient` inside the mesh, feeds
|
||||
back `tool_result` blocks, repeats until Claude answers in text.
|
||||
2. **Off-the-shelf agent framework** (evaluated: Nous Research's
|
||||
[Hermes Agent](https://hermes-agent.nousresearch.com/) — MIT, self-hostable,
|
||||
multi-channel personal assistant with memory/subagents/browsing/code-exec).
|
||||
Rejected for this role: it is a desktop/personal-assistant framework with
|
||||
chat-platform frontends, not an embeddable service — there is no clean
|
||||
`/chat` API for the control-room UI to stream from; the oikos-native
|
||||
integration (sessions in Postgres, `agent_activity` + correlation-id joins,
|
||||
approval rail) would not exist; and its browsing/code-exec surface is far
|
||||
larger than a loop confined to the 28 MCP tools. Note it (or any MCP
|
||||
client) remains usable as an *external* agent against the MCP endpoint —
|
||||
that works today and is orthogonal to the resident agent.
|
||||
3. **Hand-rolled agent loop** (~200 lines): Hermes calls the LLM API with
|
||||
tool definitions translated from `tools/list`, executes returned tool
|
||||
calls through its *existing* `mcpClient` inside the mesh, feeds results
|
||||
back, repeats until the model answers in text. Only conversation text
|
||||
leaves the mesh; the tool transport stays private; the agent is confined
|
||||
to the MCP surface by construction.
|
||||
|
||||
**Decision: (2).** MCP tool `inputSchema` is already JSON Schema — the exact
|
||||
format the Anthropic `tools` parameter expects — so translation is a field
|
||||
rename. Only the conversation text ever leaves the mesh; the tool transport
|
||||
stays private. Use the official `anthropic-sdk-go`.
|
||||
### LLM provider: OpenRouter, default model DeepSeek V4 Flash
|
||||
|
||||
- **OpenRouter** (OpenAI-compatible Chat Completions API) rather than a
|
||||
single-vendor SDK: use the official `openai-go` client with
|
||||
`base_url=https://openrouter.ai/api/v1` and `OPENROUTER_API_KEY`. Model
|
||||
choice becomes one env var; any OpenRouter model can be trialed.
|
||||
- **Default model: `deepseek/deepseek-v4-flash`** — supports tool calling,
|
||||
1M context, $0.09/M input / $0.18/M output. Use OpenRouter's **Exacto**
|
||||
routing (highest measured tool-calling accuracy) since tool calls are this
|
||||
agent's entire job; low temperature; strict system prompt.
|
||||
- MCP tool `inputSchema` is already JSON Schema — exactly what
|
||||
`tools[].function.parameters` expects — so translation is a field rename.
|
||||
- **Privacy:** conversation text and tool results (hostnames, log excerpts)
|
||||
transit OpenRouter and the upstream provider. Pin OpenRouter provider
|
||||
preferences to ZDR / no-training providers in the request payload.
|
||||
- A flash-class MoE will be weaker than frontier models on long multi-step
|
||||
chains; mitigations are the iteration cap, Exacto routing, and bumping
|
||||
`HERMES_MODEL` per-deployment when a task warrants it.
|
||||
|
||||
## Design
|
||||
|
||||
@@ -55,18 +84,18 @@ stays private. Use the official `anthropic-sdk-go`.
|
||||
system prompt = SOUL.md content (mounted; already provisioned by tools/setup-hermes-soul.sh)
|
||||
tools = listToolsFull() // extend listTools() to return name, description, inputSchema
|
||||
loop (max 15 iterations):
|
||||
resp = messages.New(model, system, history, tools)
|
||||
if resp has tool_use blocks:
|
||||
resp = chat.completions (OpenRouter, model, system, history, tools)
|
||||
if resp has tool_calls:
|
||||
for each: result = mcpClient.callTool(name, args) // existing code path
|
||||
log to agent_activity (correlation_id = session turn id)
|
||||
append assistant msg + tool_result user msg to history
|
||||
append assistant msg + tool-role result msgs to history
|
||||
else: final text → stream to caller, persist turn
|
||||
```
|
||||
|
||||
- Model: `claude-sonnet-5` default, `HERMES_MODEL` override. `max_tokens`
|
||||
and iteration cap configurable; hard per-turn budget so a pathological
|
||||
loop can't burn the API bill.
|
||||
- `ANTHROPIC_API_KEY` from env / Infisical (same secret path as other creds).
|
||||
- Model: `deepseek/deepseek-v4-flash` default, `HERMES_MODEL` override.
|
||||
`max_tokens` and iteration cap configurable; hard per-turn budget so a
|
||||
pathological loop can't burn the API bill.
|
||||
- `OPENROUTER_API_KEY` from env / Infisical (same secret path as other creds).
|
||||
- Mutations need no new guardrails: the agent's only write path is
|
||||
`request_execution`, which flows through the existing risk-class /
|
||||
approval machinery. The agent's actor identity is `agent:hermes`.
|
||||
@@ -99,7 +128,7 @@ agent-activity page and the ops ledger join up with zero new query paths.
|
||||
### Connectivity & auth
|
||||
|
||||
- Compose: hermes already runs under the `full` profile; add
|
||||
`ANTHROPIC_API_KEY`, `DATABASE_URL` env.
|
||||
`OPENROUTER_API_KEY`, `HERMES_MODEL`, `DATABASE_URL` env.
|
||||
- Caddy: route `oikos.hubris.network/agent/*` → `hermes:8092` **behind the
|
||||
same Authentik forward_auth** as the rest of the vhost, stripping the
|
||||
`/agent` prefix. The web UI then calls same-origin `/agent/chat` — no
|
||||
@@ -133,7 +162,7 @@ The agent chat becomes the **home view** of the control room (`/ui/#/`):
|
||||
|
||||
## Milestones
|
||||
|
||||
- **H1 — loop:** `agent.go` + anthropic-sdk-go; `listToolsFull()`; `/chat`
|
||||
- **H1 — loop:** `agent.go` + openai-go (OpenRouter base URL); `listToolsFull()`; `/chat`
|
||||
(non-streaming JSON first); remove toy NLU, fix `/query` fallback + help.
|
||||
Verify by curl: multi-tool question ("what's degraded and what depends on
|
||||
it?") produces chained `get_health_summary` → `get_blast_radius` calls.
|
||||
@@ -154,7 +183,7 @@ migrations/0xx_agent_sessions.up.sql
|
||||
hermes/config.yaml # model, budgets; drop query_routing block
|
||||
compose/ (env), Caddyfile.oikos (/agent route)
|
||||
web/src/pages/Chat.svelte + lib/stores/chat.ts # control-room home view
|
||||
go.mod # anthropic-sdk-go
|
||||
go.mod # openai-go (OpenRouter-compatible client)
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
Reference in New Issue
Block a user