# planner-agent Asynchroniczny agent diagnozujący zdarzenia zdrowotne w homelabowej infrastrukturze. Nasłuchuje na kanałach Redis pub/sub, wysyła zdarzenie do LLM i zapisuje propozycję akcji do `actions/pending/` — gdzie musi ją zaakceptować operator. --- ## Co robi ``` Redis pub/sub LLM (Ollama → Haiku → Sonnet) health_events ──────────────► diagnoza zdarzenia world_updates ──────────────► propozycja JSON │ cooldown gate (5 min / svc_key) │ actions/pending/.json │ events///evt-*.json (typ: remediation_started) ``` **HITL invariant:** planner pisze wyłącznie do `actions/pending/`. Executor wymaga pliku w `actions/approved/` — planner nigdy tego katalogu nie dotyka. ### Obsługiwane kanały | Kanał | Źródło | Opis | |-------|--------|------| | `health_events` | node-agent, stability-agent | Zdarzenia zdrowotne kontenerów i systemu | | `world_updates` | observer (control-plane) | Zmiany w world state | ### Benign events (pomijane bez wywołania LLM) `service_healthy`, `service_recovered`, `node_online`, `deployment_completed`, `deployment_started`, `remediation_started`, `remediation_completed` --- ## Fallback chain LLM ``` 1. ollama/ timeout 8 s (lokalny GPU — SOLARIA) 2. claude-haiku-4-5-20251001 timeout 30 s (Anthropic cloud) 3. claude-sonnet-4-6 timeout 30 s (Anthropic cloud) ``` Model jest odrzucany, gdy: - przekroczy timeout - zwróci błąd sieci / API - odpowie tekstem pasującym do wzorca odmowy (`"I cannot"`, `"nie wiem"`, …) - zwróci JSON niezgodny ze schematem propozycji Metryki każdego wywołania są publikowane na kanał Redis `llm_router_metrics`. ### Schemat propozycji (JSON Schema) ```json { "action": "restart | redeploy | notify | ignore", "service": "", "node": "", "reason": "", "confidence": <0.0–1.0>, "requires_human": } ``` Mapowanie na typ executora: | LLM action | Executor type | Risk level | |------------|--------------------|------------| | restart | container_restart | low | | redeploy | redeploy | guarded | | notify | notify | low | | ignore | *(nie zapisuje)* | — | --- ## Zmienne środowiskowe Wszystkie zmienne runtime żyją w `/opt/homelab/config/planner-agent/.env` na węźle. **Nie commituj tego pliku** — nie jest w repo. ```dotenv # Ollama — lokalny GPU węzła (np. SOLARIA) # Użyj host-gateway zamiast localhost (kontener nie może sięgnąć hosta przez localhost) OLLAMA_HOST=http://host-gateway:11434 OLLAMA_MODEL=qwen2.5-coder:14b # dowolny model dostępny w ollama list # Redis na piha REDIS_URL=redis://100.108.208.3:6379 # Tożsamość noda NODE_NAME=solaria # Cooldown między propozycjami dla tego samego serwisu COOLDOWN_SECONDS=300 # Ścieżka do runtime state RUNTIME_PATH=/opt/homelab # Opcjonalnie — wymagane do cloud fallback (haiku/sonnet) # ANTHROPIC_API_KEY=sk-ant-... ``` `ANTHROPIC_API_KEY` jest jedyną zmienną przekazywaną przez sekcję `environment` w docker-compose.yml (nie jest w env_file — sekret injektowany przez operatora). --- ## Deployment na SOLARIA ```bash # 1. Przygotuj .env na solaria ssh oskar@100.100.231.104 mkdir -p /opt/homelab/config/planner-agent cat > /opt/homelab/config/planner-agent/.env << 'EOF' OLLAMA_HOST=http://host-gateway:11434 OLLAMA_MODEL=qwen2.5-coder:14b REDIS_URL=redis://100.108.208.3:6379 NODE_NAME=solaria COOLDOWN_SECONDS=300 RUNTIME_PATH=/opt/homelab EOF # 2. Deploy przez standardowy skrypt (z SATURN) scripts/deploy/deploy.sh --service planner-agent # 3. Weryfikacja docker ps --filter name=planner-agent docker logs planner-agent --tail 30 ls -la /opt/homelab/state/planner-agent.heartbeat ``` > **Uwaga:** stage `verify` w deploy.sh może failować bezpośrednio po starcie > (race condition — heartbeat jest pisany co ~5 s). Kontener jest zdrowy gdy > `docker ps` pokazuje `(healthy)`. --- ## Uruchamianie lokalne (bez Dockera) ```bash cd services/planner-agent # Zainstaluj zależności pip install -r requirements.txt # Wyeksportuj zmienne (lub stwórz .env i użyj `export $(cat .env | xargs)`) export REDIS_URL=redis://localhost:6379 export OLLAMA_HOST=http://localhost:11434 export OLLAMA_MODEL=qwen2.5:7b export NODE_NAME=dev-local export RUNTIME_PATH=/tmp/homelab-dev export COOLDOWN_SECONDS=10 # Uruchom python src/planner.py ``` --- ## Testowanie ### Testy jednostkowe ```bash cd services/planner-agent pip install -r requirements.txt pytest pytest-asyncio pytest tests/ -v # 49 testów planner + 34 testy llm_router ``` ### Ręczny end-to-end test przez Redis ```bash # Opublikuj sztuczne zdarzenie unhealthy na kanale health_events redis-cli -h 100.108.208.3 PUBLISH health_events '{ "type": "service_unhealthy", "node": "piha", "service": "mosquitto", "severity": "error", "payload": {"exit_code": 1, "reason": "OOMKilled"}, "timestamp": "2026-05-27T20:00:00Z" }' # Obserwuj logi planera docker logs planner-agent -f # Sprawdź czy propozycja trafiła do pending ls /opt/homelab/actions/pending/ cat /opt/homelab/actions/pending/plan-piha-mosquitto-*.json ``` ### Śledzenie metryk LLM ```bash # Subskrybuj metryki routera w czasie rzeczywistym redis-cli -h 100.108.208.3 SUBSCRIBE llm_router_metrics ``` --- ## Healthcheck Skrypt `healthcheck.sh` sprawdza czy plik heartbeat `/opt/homelab/state/planner-agent.heartbeat` jest świeższy niż 300 s. Heartbeat jest pisany na górze każdej iteracji pętli (≤5 s interwał). ```bash # Ręczny test healthchecku docker exec planner-agent /bin/sh /app/healthcheck.sh ``` --- ## Struktura plików ``` services/planner-agent/ ├── src/ │ ├── planner.py # Główna pętla agenta │ └── llm_router.py # Routing LLM z fallback chain ├── tests/ │ ├── test_planner.py # 49 testów │ └── test_llm_router.py # 34 testy ├── docker-compose.yml ├── Dockerfile ├── requirements.txt ├── service.yaml # Kontrakt operacyjny dla agentów ├── healthcheck.sh └── README.md ```