~/cases/gnomaAktiv
gnoma
Warum gnoma jeden Prompt durch einen sichtbaren, provider-agnostischen Router schickt — und was der Bau eines Terminal-Agenten über das Verdienen von Vertrauen lehrt.
→Kontext
Ich betreibe mehrere LLM-Provider nebeneinander — Cloud-Modelle für harte Probleme, lokale für billige. Jeder Terminal-Agent, den ich probiert habe, hat mich auf einen Provider festgelegt oder stillschweigend das Modell gewählt.
→Problem
Modellqualität variiert je nach Aufgabe, Routing ist also wirklich nützlich. Aber Routing ohne Sichtbarkeit ist eine Black Box: Wer nicht sieht, warum ein Agent ein Modell gewählt hat, vertraut ihm keine echte Arbeit an.
→Randbedingungen
Solo-Projekt in Go, ausgeliefert als einzelne statische Binary. gnoma telefoniert nach Hause zu niemandem, und lokale Modelle sind erstklassige Bürger, kein Fallback.
→Ziele
Provider-agnostisch by Design. Die Routing-Entscheidung in jeder Runde zeigen. Sicherheit an der Content-Boundary, nicht nachgerüstet. Erweiterbar über Protokolle statt über eine Plugin-Runtime.
→Architektur
Prompts laufen über die TUI in einen Agent-Loop. Ein Multi-Armed-Bandit-Router wägt Capability-Gates, deklarierte Stärken, Latenz und Kosten ab und wählt pro Prompt einen Arm. Ein Tier-0-Kleinmodell klassifiziert Prompts und beantwortet triviale, ohne den Rechner zu verlassen. Eine Safety-Schicht scannt jede ausgehende Nachricht und jedes Tool-Ergebnis auf Secrets und Boundary-Verstöße. Tools erweitern den Loop über LSP, MCP-Server und Hooks.
- Du tippst in der TUI.
- Der Agent-Loop nimmt den Prompt entgegen; eine Safety-Schicht prüft jede ausgehende Nachricht und jedes Tool-Ergebnis.
- Der Bandit-Router wählt pro Prompt den besten Provider-Arm und zeigt seine Wahl.
- Der Prompt geht an einen von sechs Providern oder wird lokal von einem kleinen Modell beantwortet.
- Tools erweitern den Agenten über LSP, MCP-Server und Hooks.
→Trade-offs
Der Bandit-Router passt sich beobachtetem Verhalten an, ist beim Kaltstart aber konservativ. Triviale Prompts lokal zu beantworten spart Kosten und Latenz, braucht aber einen sichtbaren Override-Pfad. Erweiterung über Protokolle hält die Binary klein, macht Erweiterungen aber von externem Tooling abhängig.
→Umsetzung
Pfade werden TOCTOU-sicher kanonisiert, Unicode wird bereinigt, bevor ein Provider ihn sieht, und Language-Server-Grounding läuft über vierzehn LSP-Server. Das Ergebnis ist eine statische Binary plus Multi-Arch-Container-Image.
→Entscheidungen & Trade-offs
| Entscheidung | Warum | Trade-off |
|---|---|---|
| Multi-Armed-Bandit-Router statt statischer Regeln | Routing passt sich an beobachtete Latenz und Qualität an, statt zu raten. | Kaltstart-Wahlen sind konservativ und weniger vorhersagbar. |
| Tier-0-Modell für triviale Prompts | Einfache Prompts verlassen den Rechner nie und kosten nichts. | Fehlklassifikation braucht einen sichtbaren Override-Pfad. |
| Eine statische Binary, Erweiterung über Protokolle | Sofortige Installation, keine Laufzeit-Abhängigkeiten, kein Plugin-Manager. | Keine dynamischen Plugins; Erweiterung braucht MCP-Server oder Hooks. |
| Safety-Schicht an der Content-Boundary | Secrets und Boundary-Verstöße werden abgefangen, bevor etwas den Host verlässt. | Geringe Latenz pro Runde; gelegentliche False Positives. |
→Ergebnis
- Provider-agnostisch über Anthropic, OpenAI, Google, Mistral, Ollama und llama.cpp.
- Der Router zeigt in jeder Runde, welchen Arm er gewählt hat und warum.
- Liefert als einzelne statische Binary mit Multi-Arch-Container-Image.
→Nachweise
- Provider-Backends
- 6
- LSP-Integrationen
- 14
- Distribution
- Statisches Binary
Anthropic, OpenAI, Google, Mistral, Ollama, llama.cpp
Language-Server-Grounding
Zusätzlich ein Multi-Arch-Container-Image
→Was ich heute anders machen würde
Ich habe den Router zuerst gebaut und seine Observability zweitens. Heute würde ich mit dem Decision-Record pro Runde anfangen — der Router verdient Vertrauen nur, wenn jede Wahl im Nachhinein erklärbar bleibt.