homelab-codex-ws/docs/sessions/2026-05-27-planner-agent.md
Oskar Kapala 5ccdfa0ca6 docs: add planner-agent docs and session summary 2026-05-27
- services/planner-agent/README.md: full service doc (what it does,
  LLM fallback chain, env vars, deploy steps, local run, redis-cli
  end-to-end test, healthcheck)
- README.md: add Agent System section with all agents and their roles
- docs/sessions/2026-05-27-planner-agent.md: session summary (built
  files, architectural decisions, problems + solutions, deployment
  status, pending work)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 22:35:59 +02:00

8.8 KiB
Raw Blame History

SESSION: Budowa planner-agent — LLM-based diagnostics

DATA: 2026-05-27
REZULTAT: planner-agent działa na SOLARIA (healthy), Ollama primary, cloud fallback gotowy do włączenia


Co zostało zbudowane

services/planner-agent/src/llm_router.py

Moduł LLM routing z local-first fallback chain:

  • LLMRouter — główna klasa routingu przez litellm
  • ModelConfig — konfiguracja jednego modelu (name, timeout, api_base, extra_kwargs)
  • ModelMetrics — liczniki per model × outcome (success/fallback/error); success_rate
  • RouteResult — wynik routingu z content, model_used, attempts, latency_ms
  • AttemptRecord — zapis jednej próby (model, outcome, reason, latency_ms)
  • _extract_json_from_fence() — wydobywa JSON z bloków ```json ``` jeśli model nie odpowie czystym JSON

Domyślny chain: ollama/qwen2.5:7b (8s) → claude-haiku-4-5-20251001 (30s) → claude-sonnet-4-6 (30s)

Metryki każdego wywołania publikowane na Redis kanał llm_router_metrics.

services/planner-agent/src/planner.py

Główna pętla agenta:

  • PlannerAgent — async agent: Redis sub → diagnoza LLM → pending action file → event
  • HealthEvent — znormalizowane zdarzenie zdrowotne z Redis (node, service, event_type, severity, payload)
  • ActionProposal — propozycja akcji z pełnymi metadanymi; .to_action_file() → format executora
  • CooldownTracker — gate 5-minutowy per svc_key (node/service); NIE rejestruje jeśli LLM się wysypał
  • parse_event() — normalizuje dwa formaty wejściowe (node-agent / control-plane)
  • write_pending_action() — atomiczny zapis: .tmp → rename
  • emit_event() — zapis zdarzenia remediation_started do systemu plików (bez importów z control-plane)

Pipeline:

Redis msg → parse_event() → benign skip → cooldown gate → _propose_action() (LLM)
         → write_pending_action() → emit_event("remediation_started")

Pliki towarzyszące

Plik Opis
service.yaml Kontrakt operacyjny: owner_node=solaria, deps=redis+ollama, healthcheck=file
docker-compose.yml env_file + extra_hosts:host-gateway + ANTHROPIC_API_KEY w environment
Dockerfile python:3.11-slim, litellm, redis, jsonschema, structlog
healthcheck.sh Sprawdza wiek pliku heartbeat (max 300s)
requirements.txt litellm, redis, jsonschema, structlog
tests/test_planner.py 49 testów jednostkowych
tests/test_llm_router.py 34 testy jednostkowe

Kluczowe decyzje architektoniczne

1. HITL invariant (Human-in-the-loop)

Planner wyłącznie zapisuje do actions/pending/. Executor wymaga pliku w actions/approved/. Planner nigdy nie wykona akcji samodzielnie — to fundamentalna zasada systemu.

Implementacja: write_pending_action() pisze do pending/, żadna ścieżka w kodzie nie dotyka approved/.

2. Cooldown gate

Per svc_key (= node/service), domyślnie 5 minut. Cel: nie zalewać operatora powtórzonymi propozycjami dla tego samego serwisu.

Kluczowa decyzja: cooldown NIE jest rejestrowany jeśli cały chain LLM się wysypał. Dzięki temu kolejne zdarzenie może spróbować ponownie, zamiast być cicho zablokowanym przez 5 minut mimo że nie powstała żadna propozycja.

3. Fallback chain — local-first

Kolejność: Ollama (lokalny GPU) → Haiku → Sonnet.

Uzasadnienie:

  • Ollama nie wysyła danych do zewnętrznych serwisów; niskie opóźnienie dla prostych przypadków
  • Haiku = szybki i tani cloud fallback
  • Sonnet = ostatnia deska ratunku dla trudnych przypadków

Odrzucenie modelu na podstawie: timeout, błąd sieci, wzorzec odmowy, invalid JSON, schema error.

4. Brak importów z control-plane

services/planner-agent/ jest w pełni samodzielny. Nie importuje nic z services/control-plane/. Emisja eventów jest implementowana lokalnie (kopia logiki scripts/lib/events.py).

Uzasadnienie: planner musi działać nawet jeśli control-plane jest offline; oddzielne cykl deploymentu.

5. structlog z PrintLoggerFactory

Nie używamy structlog.stdlib.add_logger_namePrintLogger nie ma atrybutu .name. Zamiast tego łańcuch procesorów: add_log_levelTimeStamperStackInfoRendererformat_exc_infoJSONRenderer.

6. NODE_NAME czytany w czasie wywołania, nie importu

_emit_event_sync czyta NODE_NAME z modułowego NODE_NAME przy każdym wywołaniu (nie jako default parameter). Umożliwia patchowanie w testach.


Problemy napotkane i rozwiązania

Problem: localhost w kontenerze nie sięga do hosta

Kontekst: Ollama działa na SOLARIA pod localhost:11434. Kontener Docker z domyślną siecią bridge nie może sięgnąć do hosta przez localhost.

Rozwiązanie:

  1. Dodano extra_hosts: - "host-gateway:host-gateway" do docker-compose.yml
  2. .env używa OLLAMA_HOST=http://host-gateway:11434

Problem: environment vs env_file — podwójne zmienne

Kontekst: Pierwsza wersja docker-compose.yml miała wszystkie zmienne hardkodowane w sekcji environment z fallback wartościami (${VAR:-default}). Powodowało to że .env był opcjonalny a nie wymagany.

Rozwiązanie: Usunięto wszystkie zmienne runtime z environment, przeniesiono do env_file. Pozostał tylko ANTHROPIC_API_KEY w environment (opcjonalny sekret, nie powinien być w pliku na dysku).

Problem: structlog.stdlib.add_logger_name crashuje z PrintLogger

Symptom: AttributeError: 'PrintLogger' object has no attribute 'name'

Rozwiązanie: Usunięto add_logger_name z łańcucha procesorów. Nie jest kompatybilny z PrintLoggerFactory.

Problem: verify stage failuje zaraz po starcie

Symptom: deploy.sh raportuje FAILED przy verify bo heartbeat nie istnieje.

Przyczyna: Race condition — agent potrzebuje kilku sekund na uruchomienie pętli i pierwsze touch() heartbeatu.

Rozwiązanie: Nie jest to prawdziwy błąd. Docker healthcheck ma start_period: 30s. Kontener pokazuje (healthy) po 30s od startu.

Problem: git pull z divergent branches na solaria

Symptom: Solaria miała 2 lokalne commity nie będące na Forgejo + ręczne zmiany w working tree. git pull failował z "Need to specify how to reconcile divergent branches."

Rozwiązanie:

git checkout -- services/planner-agent/docker-compose.yml  # porzuć ręczne zmiany
git fetch origin
git rebase origin/master  # rebase local commits on top of master

Status deploymentu na SOLARIA

Container:  planner-agent   Up ~30m (healthy)
Image:      planner-agent-planner-agent
Node:       solaria (100.100.231.104)
Heartbeat:  /opt/homelab/state/planner-agent.heartbeat  (age 0s)

Channels subscribed:
  - health_events
  - world_updates

LLM chain:
  PRIMARY:  ollama/qwen2.5-coder:14b @ http://host-gateway:11434
  FALLBACK: claude-haiku-4-5-20251001  (disabled — brak ANTHROPIC_API_KEY)
  FALLBACK: claude-sonnet-4-6          (disabled — brak ANTHROPIC_API_KEY)

Redis:      redis://100.108.208.3:6379  ✓ connected

Co zostało na później

1. ANTHROPIC_API_KEY — cloud fallback wyłączony

Haiku i Sonnet są skonfigurowane w chain ale nie mają klucza API.
Gdy Ollama nie da rady (złożony przypadek / timeout), chain się wysypie bez fallbacku.

Aby włączyć:

ssh oskar@100.100.231.104
echo "ANTHROPIC_API_KEY=sk-ant-..." >> /opt/homelab/config/planner-agent/.env
docker compose -f ~/homelab-codex-ws/services/planner-agent/docker-compose.yml up -d

2. End-to-end test z prawdziwym eventem

Planner jest podłączony do Redis i nasłuchuje, ale żadne zdarzenie jeszcze nie przeszło przez pełną ścieżkę (LLM call → pending action → operator UI).

Test:

redis-cli -h 100.108.208.3 PUBLISH health_events '{
  "type": "service_unhealthy",
  "node": "piha",
  "service": "mosquitto",
  "severity": "error",
  "payload": {"reason": "container exited"},
  "timestamp": "2026-05-27T20:00:00Z"
}'
# Obserwuj: docker logs planner-agent -f
# Sprawdź: ls /opt/homelab/actions/pending/

3. Solaria local commits

Solaria ma 2 lokalne commity (feat: add ECC skills, fix: remove duplicate CLAUDE.md sections) które nie są na Forgejo. Zostały zrebase'owane na top of master ale nie wypchnięte. Należy je wypchnąć lub zreviewować i ewentualnie squashować.

4. Integracja z operator UI / Telegram

Propozycje w actions/pending/ nie mają jeszcze kanału notyfikacji do operatora. Telegram bot powinien wysyłać powiadomienie gdy pojawi się nowy plik w pending/.


Commity tej sesji

ff6fda1  planner-agent: use env_file, keep only ANTHROPIC_API_KEY in environment
ca37fca  Add planner-agent: LLM-powered remediation planner
         (llm_router.py, planner.py, tests, service.yaml, docker-compose.yml,
          healthcheck.sh, Dockerfile)