docs: Architekturplan, ADRs, Spezifikationen und Umsetzungsplan
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
# 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`.
|
||||
|
||||
```yaml
|
||||
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:
|
||||
1. Embedding der Eingabe über `EmbeddingClient` (LiteLLM `/v1/embeddings`, Slot `embed-default`).
|
||||
2. Kandidaten: Score pro Skill = max(cos(q, description), cos(q, trigger_i)). Davon die Top-k (k = 5).
|
||||
3. 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.
|
||||
4. Gibt es keinen Kandidaten oder liegt die Ähnlichkeit unter `threshold`, wird mit `no_skill` auf Stufe 0 geroutet.
|
||||
5. **Konflikt:** Liegt der Zweitplatzierte innerhalb von `conflict_margin` (0,02) und ist kein Reranker aktiv, wird mit `skill_conflict` auf Stufe 0 geroutet. Ist ein Reranker aktiv, wird nur das Signal gesetzt.
|
||||
6. Bei `status == demoted` oder `stage == 0` geht es auf Stufe 0, mit Skill als Hinweis.
|
||||
7. **Stufe 3/2:** Classifier bzw. Programm, ohne Slot.
|
||||
8. **Stufe 1:** Slot des Skills, falls verfügbar, sonst dessen `fallbacks`. Ist keiner verfügbar, wird mit `slot_unavailable` auf Stufe 0 geroutet.
|
||||
9. **Token-Budget** (§4): erst `full` versuchen, dann `compact`, dann die Fallback-Slots mit größerem Kontext. Passt nichts, wird mit `context_exceeded` auf Stufe 0 geroutet. Passt es auch auf Stufe 0 nicht, ist das Ergebnis ein Fehler `too_large` mit dem Signal `needs_split`.
|
||||
10. **Adapter:** Wenn `slot.supports_lora`, `skill.adapter == auto` und ein aktiver Adapter für `(slot.base_model, skill.cluster)` existiert, wird `model = adapter.name` gesetzt, sonst `model = slot.litellm_model`.
|
||||
|
||||
`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 Skill `execution: llm` als 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, wird `classifier_low_confidence` gemeldet.
|
||||
|
||||
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/run` eine 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 |
|
||||
|
||||
Authentifizierung: Header `Authorization: Bearer <KNASTER_API_TOKEN>` auf allen `/v1/*`-Pfaden.
|
||||
@@ -0,0 +1,50 @@
|
||||
# SPEC-healer: Deterministisches Autohealing
|
||||
|
||||
Paket: `knaster.healer`.
|
||||
|
||||
## 1. Eingang
|
||||
|
||||
Alertmanager-Webhook (Version 4) an `POST /v1/healer/alertmanager`. Pro Alert werden `status` (firing/resolved), `labels`, `annotations` und `fingerprint` verarbeitet.
|
||||
|
||||
## 2. Regeln (`config/healer-rules.yaml`)
|
||||
|
||||
```yaml
|
||||
rules:
|
||||
- id: slot-endpoint-down
|
||||
match: {alertname: SlotEndpointDown} # alle Labels müssen gleich sein
|
||||
ladder:
|
||||
- {action: restart_container, params: {host: "{node}", container: "{container}"}, attempts: 2, cooldown: 5m}
|
||||
- {action: restart_vm, params: {vmid: "{vmid}", pve_node: "{pve_node}"}, attempts: 1, cooldown: 15m}
|
||||
- {action: mark_slot, params: {slot: "{slot}", status: degraded}, attempts: 1}
|
||||
on_exhausted: notify
|
||||
on_resolved: [{action: mark_slot, params: {slot: "{slot}", status: ok}, only_if_set_by_healer: true}]
|
||||
```
|
||||
|
||||
`{label}` wird aus den Alert-Labels ersetzt. Fehlt ein Label, wird die Stufe übersprungen und im Ergebnis vermerkt.
|
||||
|
||||
## 3. Ablauf pro Alert (firing)
|
||||
|
||||
1. Erste passende Regel suchen (Reihenfolge der Datei). Passt keine, wird nur benachrichtigt (`unmatched`).
|
||||
2. Zustand pro `(rule_id, fingerprint)`: Versuche pro Stufe und Zeitpunkt der letzten Aktion.
|
||||
3. Ist der Cooldown der letzten Aktion noch nicht vorbei, passiert nichts (`cooldown`).
|
||||
4. Sonst wird die erste Stufe genommen, deren Versuchsbudget noch nicht erschöpft ist. Vor der Ausführung entscheidet die **Policy** (SPEC-maintenance §2): Bei `auto` wird ausgeführt, bei `approve` entsteht ein Vorschlag (Quelle `healer`) und die Stufe gilt als verbraucht.
|
||||
5. Sind alle Stufen erschöpft, wird einmalig benachrichtigt (`exhausted`).
|
||||
6. Ein globaler Schutz begrenzt die Infrastruktur-Aktionen auf `max_actions_per_hour` (Standard 10). Darüber wird nur noch benachrichtigt.
|
||||
|
||||
**resolved:** Die `on_resolved`-Aktionen laufen, und der Zustand des Fingerprints wird gelöscht.
|
||||
|
||||
## 4. Treiber (`knaster.healer.drivers`)
|
||||
|
||||
| Aktion | Treiber | API |
|
||||
|---|---|---|
|
||||
| `restart_container` | Portainer | `POST /api/endpoints/{endpoint}/docker/containers/{container}/restart` |
|
||||
| `restart_vm` | Proxmox | `POST /api2/json/nodes/{pve_node}/qemu/{vmid}/status/reboot` |
|
||||
| `reboot_host` | Proxmox | `POST /api2/json/nodes/{pve_node}/status` (`command=reboot`) |
|
||||
| `mark_slot` | Registry | Laufzeitzustand |
|
||||
| `notify` | Webhook (n8n) | `POST NOTIFY_WEBHOOK_URL` |
|
||||
|
||||
Alle Treiber haben einen **Dry-Run**-Modus (Standard: an). Ein globales `KNASTER_DRY_RUN=false` schaltet die Ausführung frei, zusätzlich muss der Treiber pro Aktion in `config/healer-rules.yaml` unter `enabled_actions` aufgeführt sein.
|
||||
|
||||
## 5. Mitgelieferte Alerts (`deploy/prometheus/alerts.yml`)
|
||||
|
||||
`SlotEndpointDown`, `SlotCanaryFailing`, `GpuXidError`, `GpuTemperatureHigh`, `VllmKvCacheSaturated`, `NodeDown`, `DiskAlmostFull`.
|
||||
@@ -0,0 +1,34 @@
|
||||
# SPEC-inventory: Inventar, Modell-Katalog, Placement
|
||||
|
||||
Paket: `knaster.inventory`. CLI: `knaster inventory collect`, `knaster placement plan`.
|
||||
|
||||
## 1. Node-Profil
|
||||
|
||||
```yaml
|
||||
name: pve-ada
|
||||
cpu: {model: "...", cores: 8, threads: 16}
|
||||
memory_gb: 64
|
||||
disks: [{name: nvme0n1, size_gb: 256, rotational: false}]
|
||||
gpus:
|
||||
- {index: 0, name: "NVIDIA RTX 4000 Ada Generation", memory_gb: 20, compute_cap: 8.9, driver: "..."}
|
||||
```
|
||||
|
||||
Datenquellen: `nvidia-smi --query-gpu=index,name,memory.total,compute_cap,driver_version --format=csv,noheader,nounits`, `/proc/meminfo`, `/proc/cpuinfo` bzw. `lscpu`, `lsblk -J -b -d -o NAME,SIZE,ROTA`. Die Parser sind reine Funktionen auf dem jeweiligen Text und testbar. `collect` ruft die Befehle lokal auf.
|
||||
|
||||
## 2. Katalog (`catalog/models.yaml`, `catalog/roles.yaml`)
|
||||
|
||||
Ein Modell hat `kind` (llm, embedding, reranker), `vram_gb` (Gewichte plus Mindest-KV-Cache), `min_cc`, `engines`, `context`, `lora`.
|
||||
Eine Rolle hat `needs` (`engine`, `min_vram_gb`, `min_cc`, `max_cc?`, `gpu: true|false`), `prefer` (z. B. `smallest_fitting`), `count` und `slots`.
|
||||
|
||||
## 3. Placement (`plan(nodes, roles, models) -> PlacementPlan`)
|
||||
|
||||
Deterministisch:
|
||||
1. Alle GPUs aller Nodes zu einer Liste `(node, gpu)` zusammenfassen.
|
||||
2. Rollen nach `priority` (aufsteigend) abarbeiten. Für jede Rolle werden `count` GPUs gesucht, die `needs` erfüllen und noch frei sind.
|
||||
- `prefer: largest` wählt die GPU mit dem meisten VRAM (für LLM-Rollen).
|
||||
- `prefer: smallest_fitting` wählt die kleinste passende GPU (für Embedding und Rerank). So bleiben große Karten frei.
|
||||
3. Rollen mit `gpu: false` bzw. `cpu_fallback: true` werden ohne passende GPU den CPU-Nodes zugewiesen, sortiert nach RAM.
|
||||
4. Pro zugewiesener Rolle werden die Katalogmodelle gelistet, die passen (Engine, VRAM, CC).
|
||||
5. Ergebnis: `assignments[{role, node, gpu_index?, models_fitting[]}]` und `unplaced[{role, reason}]`.
|
||||
|
||||
Der Plan ist der Input für `platform/proxmox` (welche VM-Rolle auf welchem Host mit welchem Resource Mapping) und für die Registry (welche Slots existieren).
|
||||
@@ -0,0 +1,46 @@
|
||||
# SPEC-lifecycle: Kandidaten, Graduierung, Training
|
||||
|
||||
Pakete: `knaster.lifecycle`, `knaster.graduation`.
|
||||
|
||||
## 1. Kandidat
|
||||
|
||||
```
|
||||
id, kind: base_model | adapter | quantization | embedding_model | engine
|
||||
slot: str # betroffener Slot (bei adapter: Slot, dessen Basis passt)
|
||||
target: {...} # z. B. {base_model: "qwen3-14b-awq"} oder {cluster: "rechnungen"}
|
||||
baseline: {pass_rate?} # Messwert der aktuellen Version
|
||||
state, metrics{}, history[{ts, from, to, note}]
|
||||
```
|
||||
|
||||
## 2. Zustände
|
||||
|
||||
```
|
||||
proposed ──(Freigabe)──▶ building ──report(build_ok)──▶ evaluating
|
||||
evaluating ──report(eval: pass_rate ≥ baseline − tolerance)──▶ shadow (sonst ▶ rejected)
|
||||
shadow ──report(shadow: agreement ≥ min_agreement, n ≥ min_n)──▶ canary (sonst ▶ rejected)
|
||||
canary ──report(canary: pass_rate ≥ baseline − tolerance, n ≥ min_n)──▶ awaiting_promotion (sonst ▶ rejected)
|
||||
awaiting_promotion ──(Freigabe)──▶ promoted
|
||||
promoted ──rollback──▶ rolled_back
|
||||
jeder Zustand ──report(error)──▶ failed
|
||||
```
|
||||
|
||||
Grenzwerte stehen in `config/lifecycle.yaml`: `tolerance` 0,02, `min_agreement` 0,85, `min_n` 50, `canary_share` 0,1.
|
||||
|
||||
Die Freigaben laufen über die Policy (`create_candidate`, `promote_candidate`). **Promote** schreibt in den Registry-Laufzeitzustand bzw. in die Adapterliste:
|
||||
- `base_model`: Slot `base_model` wird umgestellt, alle Adapter der alten Basis werden `retired`, und für jeden Cluster mit Adapter entsteht ein Kandidat `adapter` (Neutraining).
|
||||
- `adapter`: Der neue Adapter wird `active`, der bisher aktive für denselben Cluster und dieselbe Basis wird `retired` (bleibt als Rollback-Ziel erhalten).
|
||||
- **Rollback** stellt den Zustand vor dem Promote wieder her (gespeichert in `history`).
|
||||
|
||||
Die Jobs selbst (Download, Quantisierung, Training, Eval) laufen **extern**, als Job-Runner auf `gpu-flex`. Sie melden ihr Ergebnis per `POST /v1/lifecycle/candidates/{id}/report`. Der `JobRunner` im Service stößt den Job an: Im Dry-Run wird nur protokolliert, sonst geht ein Webhook an n8n bzw. den Runner.
|
||||
|
||||
## 3. Graduierung (`knaster.graduation`)
|
||||
|
||||
`graduate(skill, variants[], run_fn, verify_fn, threshold=0.9, min_variants=10) -> GraduationReport`
|
||||
- Jede Variante wird mit `stage = 1` auf dem Skill-Slot ausgeführt (`run_fn`) und geprüft (`verify_fn`).
|
||||
- `passed = pass_rate ≥ threshold` und `n ≥ min_variants`.
|
||||
- Bei Erfolg entsteht ein Vorschlag `promote_skill{skill, stage: 1}` (Policy: approve), bei Misserfolg ein Bericht mit den fehlgeschlagenen Varianten. Diese gehen an das starke Modell, das den Skill überarbeitet.
|
||||
- Die Varianten erzeugt das starke Modell (Hermes) und schickt sie an `POST /v1/skills/{name}/graduate`.
|
||||
|
||||
## 4. Adapter-Training (extern, Vertrag)
|
||||
|
||||
Eingang für den Trainingsjob: Export `cases` (split = train, passed = true, skill.cluster = X) plus optional `preference_pairs`. Ausgang: Adapter-Verzeichnis und `report(build_ok, adapter_name)`. Die Eval nutzt die Fälle mit split = eval desselben Clusters.
|
||||
@@ -0,0 +1,77 @@
|
||||
# SPEC-maintenance: Pflege-Agent, Vorschläge, Freigabe-Richtlinie
|
||||
|
||||
Paket: `knaster.maintenance`, `knaster.policy`.
|
||||
|
||||
## 1. Vorschlag (`Proposal`)
|
||||
|
||||
```
|
||||
id: "P-<laufende Nr>"
|
||||
kind: promote_skill | demote_skill | merge_skills | retire_skill | mark_slot | restart_container |
|
||||
restart_vm | reboot_host | create_candidate | promote_candidate | rollback_candidate |
|
||||
train_adapter | recalibrate_context
|
||||
params: {...}
|
||||
summary: str # menschenlesbar
|
||||
reason: str
|
||||
risk: low | medium | high
|
||||
state: open | approved | rejected | executed | failed | expired
|
||||
source: routine | command | healer | lifecycle
|
||||
created_at, decided_at, decided_by, result
|
||||
```
|
||||
|
||||
## 2. Freigabe-Richtlinie (`config/policy.yaml`)
|
||||
|
||||
```yaml
|
||||
default: approve
|
||||
actions:
|
||||
demote_skill: {mode: auto}
|
||||
mark_slot: {mode: auto}
|
||||
restart_container: {mode: auto}
|
||||
restart_vm: {mode: auto}
|
||||
recalibrate_context:{mode: auto}
|
||||
promote_skill: {mode: approve}
|
||||
...
|
||||
reboot_host: {mode: approve, window: "02:00-05:00"}
|
||||
```
|
||||
|
||||
`Policy.decide(kind, now) -> auto | approve | deny`. Liegt eine Aktion mit `window` außerhalb des Zeitfensters, wird sie zwar freigegeben, aber erst im nächsten Fenster ausgeführt (`deferred`). Ein Vorschlag mit Modus `auto` wird sofort ausgeführt und mit `decided_by = policy` protokolliert.
|
||||
|
||||
## 3. Service-Routine (`MaintenanceAgent.run_routine`)
|
||||
|
||||
Befunde werden aus diesen Quellen gesammelt (alle Grenzwerte in `config/maintenance.yaml`):
|
||||
|
||||
| Befund | Bedingung | Vorschlag |
|
||||
|---|---|---|
|
||||
| Skill-Drift | Pass-Rate der letzten N Fälle auf Stufe ≥ 1 < `demote_below` (0,80), n ≥ `min_cases` (20) | `demote_skill` (auto) |
|
||||
| Graduierungs-Kandidat | Skill auf Stufe 0, ≥ `min_cases` Fälle, Pass-Rate auf Stufe 0 ≥ 0,95 | `promote_skill` → Graduierungslauf (approve) |
|
||||
| Ähnliche Skills | cos(description_a, description_b) ≥ `merge_similarity` (0,92) | `merge_skills` (approve) |
|
||||
| Slot degradiert | Laufzeitzustand ≠ ok seit > 1 h | Hinweis + `mark_slot ok` nach Prüfung (approve) |
|
||||
| Kontext-Ausweicher | Signal `context_exceeded` oder `output_truncated` ≥ `context_signal_limit` (10) pro Slot/Woche | `recalibrate_context` (auto) |
|
||||
| Adapter-Bedarf | Cluster mit ≥ `adapter_min_cases` (300) verifizierten Train-Fällen und Pass-Rate auf Stufe 1 < 0,9 | `train_adapter` (approve) |
|
||||
| Offene Kandidaten | Lifecycle-Kandidat wartet auf Freigabe | Verweis auf `promote_candidate` |
|
||||
|
||||
Das Ergebnis ist ein `RoutineReport{findings[], proposals[], auto_executed[], text}`. `text` ist ein deutscher Bericht für den Chat.
|
||||
|
||||
## 4. Befehle per Prompt (`MaintenanceAgent.handle_command`)
|
||||
|
||||
Feste Grammatik (Groß-/Kleinschreibung egal, deutsch und kurze englische Synonyme):
|
||||
|
||||
| Muster | Wirkung |
|
||||
|---|---|
|
||||
| `status` / `hilfe` | Überblick bzw. Befehlsliste |
|
||||
| `routine` / `starte (die) service-routine` | Service-Routine |
|
||||
| `vorschläge` / `zeige vorschläge` | offene Vorschläge |
|
||||
| `genehmige P-3 [P-5 …]` / `approve P-3` | Freigabe und Ausführung |
|
||||
| `genehmige alle (mit )?risiko niedrig` | alle offenen Vorschläge mit `risk == low` |
|
||||
| `lehne P-3 ab[: Grund]` / `reject P-3` | Ablehnung |
|
||||
| `wechsle <slot> auf <base_model>` | Vorschlag `create_candidate{kind: base_model}` |
|
||||
| `trainiere adapter für <cluster>` | Vorschlag `train_adapter` |
|
||||
| `rollback <slot>` | Vorschlag `rollback_candidate` |
|
||||
| `stufe <skill> zurück` / `degradiere <skill>` | Vorschlag `demote_skill` |
|
||||
| `markiere <slot> (als)? (ok\|degraded\|unavailable)` | Vorschlag `mark_slot` |
|
||||
|
||||
Bei unbekanntem Text wird, falls konfiguriert, ein `IntentMapper` (LLM, Slot `tier1-general`) gefragt, der **nur** einen der obigen Befehle als String zurückgeben darf. Der Agent antwortet dann: „Meinst du: `<befehl>`? Antworte mit ‚ja‘.“ Ein nachfolgendes `ja` in derselben Sitzung führt den Befehl aus, einen neu erzeugten Vorschlag aber ebenfalls nur bis zur Freigabestufe.
|
||||
Aus Prompt-Aufträgen erzeugte Vorschläge werden **nie** sofort ausgeführt, auch wenn die Richtlinie `auto` sagt. Der Mensch bestätigt sie mit `genehmige`.
|
||||
|
||||
## 5. Ausführung
|
||||
|
||||
`ActionExecutor.execute(proposal) -> result`. Aktionen sind registrierte Handler. Infrastruktur-Aktionen (`restart_*`, `reboot_host`) gehen an dieselben Treiber wie beim Healer (SPEC-healer §4). Standard ist **Dry-Run**: Das Ergebnis beschreibt dann, was getan worden wäre.
|
||||
Reference in New Issue
Block a user