~/cases/gnomaActive

gnoma

Why gnoma routes every prompt through a visible, provider-agnostic router — and what building a terminal agent teaches about earning developer trust.

Solo Engineer · Personal Project · 2025 - present

Context

I run several LLM providers side by side — cloud models for hard problems, local models for cheap ones. Every terminal agent I tried locked me into one provider or silently picked the model for me.

Problem

Model quality varies per task, so routing is genuinely useful. But routing without visibility is a black box: a developer who cannot see why an agent picked a model will not trust it with real work.

Constraints

Solo project in Go, shipped as a single static binary. gnoma phones home to nothing, and local models are first-class citizens, not a fallback.

Goals

Provider-agnostic by design. Show the routing decision on every turn. Security at the content boundary, not bolted on. Extensible through protocols instead of a plugin runtime.

Architecture

Prompts enter through the TUI into an agent loop. A multi-armed bandit router weighs capability gates, declared strengths, latency, and cost, then selects an arm per prompt. A tier-0 small local model classifies prompts and answers trivial ones without leaving the machine. A safety layer scans every outgoing message and tool result for secrets and content-boundary violations. Tools extend the loop through LSP, MCP servers, and hooks.

Trade-offs

The bandit router adapts to observed behaviour but is conservative on cold start. Answering trivial prompts locally saves cost and latency but needs an override path for misclassification. Extending through protocols keeps the binary small but means extensions depend on external tooling.

Implementation

Paths are canonicalised TOCTOU-safe, Unicode is sanitized before it reaches a provider, and language-server grounding runs across fourteen LSP servers. The result ships as one static binary plus a multi-arch container image.

  • You type in the TUI.
  • The agent loop receives the prompt; a safety layer scans every outgoing message and tool result.
  • The bandit router picks the best provider arm per prompt and shows its choice.
  • The prompt goes to one of six providers, or is answered locally by a small model.
  • Tools extend the agent through LSP, MCP servers, and hooks.
gnoma · request path

Decisions & trade-offs

DecisionWhyTrade-off
Multi-armed bandit router instead of static rulesRouting adapts to observed latency and quality instead of guessing.Cold-start picks are conservative and less predictable.
Tier-0 local model for trivial promptsSimple prompts never leave the machine or cost anything.Misclassification needs a visible override path.
Single static binary, extensions via protocolsInstant install, no runtime dependencies, no plugin manager.No dynamic plugins; extension requires MCP servers or hooks.
Safety layer at the content boundarySecrets and boundary violations are caught before anything leaves the host.Small per-turn latency; occasional false positives.

Result

  • Provider-agnostic across Anthropic, OpenAI, Google, Mistral, Ollama, and llama.cpp.
  • The router shows which arm it picked and why on every turn.
  • Ships as a single static binary with a multi-arch container image.

Evidence

Provider backends
6

Anthropic, OpenAI, Google, Mistral, Ollama, llama.cpp

LSP integrations
14

Language-server grounding

Distribution
Static binary

Plus a multi-architecture container image

What I'd change today

I built the router first and its observability second. Today I would start with the per-turn decision record — the router only earns trust if every pick stays explainable after the fact.