9.7 KiB
SPEC-core: Registry, Skills, Router, Ausführung, Verifier
Paket: core/src/knaster. Alle Schnittstellen zu Modellen laufen über das OpenAI-kompatible API von LiteLLM.
1. Registry (knaster.registry)
Deklarativer Teil (Git): sites/<cluster>/registry.yaml. Laufzeitzustand (DB): slot_state.
slots:
tier1-general:
tier: 1 # 0 = stark/extern, 1 = schwaches LLM, (Stufe 2/3 brauchen keinen Slot)
device: gpu # gpu | cpu | external
engine: vllm # vllm | llamacpp | openrouter | ...
node: gpu-main # Rolle/Node, für Healer und Placement
litellm_model: tier1-general
base_model: qwen3-14b-awq
max_context: 32768
effective_context: 24000 # Startwert; Kalibrierung überschreibt im Laufzeitzustand
supports_lora: true
fallbacks: [tier1-flex] # nur bei Infrastruktur-Fehlern, nicht bei Qualitätsfehlern
adapters:
- name: rechnungen-v3 # = Modellname, unter dem vLLM den Adapter bereitstellt
base_model: qwen3-14b-awq
cluster: rechnungen
version: 3
status: active # active | candidate | retired
Laufzeitzustand pro Slot: status ∈ {ok, degraded, unavailable}, effective_context (kalibriert, optional), reason, updated_at.
Für den Router gilt: Ein Slot ist verfügbar, wenn status == ok.
2. Skills (knaster.skills)
Dateiformat ist SKILL.md mit YAML-Frontmatter, kompatibel zu agentskills/Hermes (name, description). Eigene Felder stehen unter metadata.knaster:
| Feld | Typ | Default | Bedeutung |
|---|---|---|---|
version |
int | 1 | |
status |
draft/active/demoted/retired | draft | nur active und demoted werden geroutet |
stage |
0–3 | 0 | freigegebene Ausführungsstufe |
execution |
llm/program/classifier | llm | Art der Ausführung ab Stufe ≥ 1 |
slot |
str | tier1-general | Ziel-Slot für Stufe 1 |
cluster |
str | = name | Adapter-Cluster |
adapter |
auto/none | auto | Adapter verwenden, falls einer aktiv ist |
threshold |
float | 0.80 | minimale Ähnlichkeit |
triggers |
list[str] | [] | zusätzliche Beispielformulierungen fürs Embedding |
output.format |
text/json | text | |
output.schema |
JSON-Schema | – | für den Verifier |
verifier |
list | [] | Prüfungen, siehe §5 |
max_output_tokens |
int | 1024 | |
program |
str | – | Name des registrierten Programms (Stufe 2) |
classifier |
str | – | Name des Classifiers (Stufe 3) |
Body: Markdown-Anleitung. Abschnitte, deren Überschrift mit ## Beispiele beginnt, gelten als optional. Die kompakte Variante des Skills enthält sie nicht.
demoted bedeutet: Der Skill wird erkannt, aber auf Stufe 0 ausgeführt, mit dem Skill als Hinweis.
3. Router (knaster.router)
Eingabe ist RouteRequest{input: str, hints?: {skill?: str}}. Ausgabe ist RouteDecision:
stage, slot?, model?, skill?, skill_version?, adapter?, prompt_variant (full|compact|none),
similarity?, estimated_tokens, reasons[], signals[], attempt
Ablauf:
- Embedding der Eingabe über
EmbeddingClient(LiteLLM/v1/embeddings, Slotembed-default). - Kandidaten: Score pro Skill = max(cos(q, description), cos(q, trigger_i)). Davon die Top-k (k = 5).
- Optionales Reranking der Top-k (
Reranker, llama.cpp/v1/rerank). Die Reihenfolge folgt dann dem Rerank-Score, der Schwellwert prüft weiterhin die Cosine-Ähnlichkeit. - Gibt es keinen Kandidaten oder liegt die Ähnlichkeit unter
threshold, wird mitno_skillauf Stufe 0 geroutet. - Konflikt: Liegt der Zweitplatzierte innerhalb von
conflict_margin(0,02) und ist kein Reranker aktiv, wird mitskill_conflictauf Stufe 0 geroutet. Ist ein Reranker aktiv, wird nur das Signal gesetzt. - Bei
status == demotedoderstage == 0geht es auf Stufe 0, mit Skill als Hinweis. - Stufe 3/2: Classifier bzw. Programm, ohne Slot.
- Stufe 1: Slot des Skills, falls verfügbar, sonst dessen
fallbacks. Ist keiner verfügbar, wird mitslot_unavailableauf Stufe 0 geroutet. - Token-Budget (§4): erst
fullversuchen, danncompact, dann die Fallback-Slots mit größerem Kontext. Passt nichts, wird mitcontext_exceededauf Stufe 0 geroutet. Passt es auch auf Stufe 0 nicht, ist das Ergebnis ein Fehlertoo_largemit dem Signalneeds_split. - Adapter: Wenn
slot.supports_lora,skill.adapter == autound ein aktiver Adapter für(slot.base_model, skill.cluster)existiert, wirdmodel = "<slot.litellm_model>/<adapter.name>"gesetzt (LiteLLM-Wildcard-Route auf den vLLM des Slots), sonstmodel = slot.litellm_model. - Canary: Hat der Slot im Laufzeitzustand ein
canary_model(vom Lifecycle gesetzt), geht der Anteilcanary_shareder passenden Anfragen auf den Kandidaten. Bei Adapter-Kandidaten gilt das nur für den Cluster des Kandidaten. Die Entscheidung trägt danncanary: true.
escalate(decision, reason):
- Bei Infrastruktur-Fehlern (
endpoint_error,context_overflow) wird auf derselben Stufe der nächste Fallback-Slot genommen, sonst wird eine Stufe höher gegangen. - Qualitätsfehler (
verifier_failed,invalid_format,degenerate_output,output_truncated,classifier_low_confidence,program_error) eskalieren: 3/2 → 1 (falls der Skillexecution: llmals Rückfall erlaubt, sonst → 0) und 1 → 0. - Maximal 3 Eskalationen. Auf Stufe 0 ist danach Schluss, das Ergebnis lautet
failed.
4. Token-Budget (knaster.budget)
estimate_tokens(text) ist ein konservativer Schätzer (ceil(len/3)) und ersetzbar durch einen echten Tokenizer pro Slot.
Bedarf = Systemprompt + Eingabe + max_output_tokens + Overhead (64).
Ein Prompt passt, wenn Bedarf ≤ effective_context des Slots (Laufzeitwert vor Registry-Wert).
5. Ausführung (knaster.executor) und Verifier (knaster.verifier)
Executor:
- Stufe 1/0: Nachrichten bauen (System = Skill-Body in der gewählten Variante plus Formatanweisung, User = Eingabe), dann
POST /v1/chat/completions. - Stufe 2: registriertes Programm
fn(input:str) -> str. - Stufe 3:
EmbeddingClassifier.predict(vector) -> (label, confidence). Liegt die Konfidenz unter dem Minimum, wirdclassifier_low_confidencegemeldet.
Erkannte Signale (knaster.problems.Signal):
| Signal | Erkennung |
|---|---|
output_truncated |
finish_reason == "length" |
degenerate_output |
Anteil wiederholter 4-Gramme > 0,5 bei ≥ 40 Wörtern |
invalid_format |
Format json, aber nicht parsebar |
context_overflow |
HTTP 400 mit „context length“ / „maximum context“ / „too many tokens“ im Text |
endpoint_error |
Timeout, Verbindungsfehler, HTTP 5xx/429 |
Verifier-Prüfungen (metadata.knaster.verifier): json_schema (gegen output.schema), regex {pattern}, contains {value}, not_contains {value}, max_chars {value}, judge {slot, rubric} (das LLM antwortet PASS oder FAIL: Grund).
Ergebnis ist VerifyResult{passed, checks[{type, passed, detail}]}. Bei Format json wird json_schema immer geprüft, falls ein Schema vorhanden ist.
6. Fälle und Datensätze (knaster.cases)
- Tabelle
cases: id, ts, input, skill, skill_version, stage, slot, model, adapter, messages (JSON, modellneutral), output, passed, signals, split (train/eval, deterministisch über sha256(input) mod 10 < 2 → eval), origin (route/graduation/shadow). - Tabelle
preference_pairs: Entsteht, wenn in einem/v1/runeine Stufe ≥ 1 durch den Verifier fällt und eine höhere Stufe besteht. Inhalt: prompt messages,rejected,chosen, skill, split. - Export als JSONL im Format
{"messages":[...], "meta":{...}}bzw.{"prompt":[...],"chosen":"...","rejected":"..."}.
7. Kalibrierung (knaster.calibration)
effective_context(observations, target=0.9, min_n=20, buckets=[2k,4k,8k,16k,32k,64k,128k]): Die Beobachtungen (Prompt-Tokens, bestanden) werden in Buckets gruppiert. Ergebnis ist die Obergrenze des größten Buckets, für den gilt: Dieser und alle kleineren Buckets mit n ≥ min_n haben eine Pass-Rate ≥ target. Ohne ausreichende Daten ist das Ergebnis None, und der Registry-Wert bleibt.
8. HTTP-API (knaster.api)
| Methode | Pfad | Zweck |
|---|---|---|
| GET | /health |
Liveness |
| GET | /metrics |
Prometheus |
| POST | /v1/route |
RouteDecision |
| POST | /v1/run |
komplette Schleife: Route → Ausführung → Verifier → Eskalation, dazu Fall speichern |
| GET | /v1/registry |
Slots plus Laufzeitzustand |
| POST | /v1/registry/slots/{slot}/state |
Zustand setzen (Healer/Mensch) |
| POST | /v1/maintenance/routine |
Service-Routine starten (SPEC-maintenance) |
| POST | /v1/maintenance/command |
Prompt-Befehl |
| GET | /v1/maintenance/proposals |
offene Vorschläge |
| POST | /v1/healer/alertmanager |
Alertmanager-Webhook (SPEC-healer) |
| GET/POST | /v1/lifecycle/candidates |
Kandidaten (SPEC-lifecycle) |
| POST | /v1/lifecycle/candidates/{id}/report |
Ergebnis eines Jobs melden |
| POST | /v1/skills/{name}/graduate |
Graduierungslauf mit mitgelieferten Varianten |
| POST | /v1/skills/reload |
Skill-Dateien neu laden (nach Git-Pull) |
| POST | /v1/maintenance/proposals/{seq}/approve bzw. /reject |
Freigabe per Knopf (n8n/UI) statt per Prompt |
9. Canary-Prober (knaster.canary)
Läuft im Service-Prozess (KNASTER_CANARY_INTERVAL, Standard 60 s, 0 = aus). Er prüft jeden Slot außer Stufe 0 und Rerank mit einer Mini-Anfrage („Antworte nur mit OK“) bzw. einem Embedding. Daraus entstehen die Metriken knaster_slot_endpoint_up, knaster_slot_canary_ok und knaster_slot_canary_latency_seconds, jeweils mit den Labels slot, node, docker_host, container, pve_node, vmid aus ops der Registry. Daraus leiten sich die Alerts SlotEndpointDown und SlotCanaryFailing für den Healer ab.
Authentifizierung: Header Authorization: Bearer <KNASTER_API_TOKEN> auf allen /v1/*-Pfaden.