~/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.
→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.
→Decisions & trade-offs
| Decision | Why | Trade-off |
|---|---|---|
| Multi-armed bandit router instead of static rules | Routing adapts to observed latency and quality instead of guessing. | Cold-start picks are conservative and less predictable. |
| Tier-0 local model for trivial prompts | Simple prompts never leave the machine or cost anything. | Misclassification needs a visible override path. |
| Single static binary, extensions via protocols | Instant install, no runtime dependencies, no plugin manager. | No dynamic plugins; extension requires MCP servers or hooks. |
| Safety layer at the content boundary | Secrets 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
- LSP integrations
- 14
- Distribution
- Static binary
Anthropic, OpenAI, Google, Mistral, Ollama, llama.cpp
Language-server grounding
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.