- 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>
8.8 KiB
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 litellmModelConfig— konfiguracja jednego modelu (name, timeout, api_base, extra_kwargs)ModelMetrics— liczniki per model × outcome (success/fallback/error); success_rateRouteResult— wynik routingu zcontent,model_used,attempts,latency_msAttemptRecord— 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 → eventHealthEvent— znormalizowane zdarzenie zdrowotne z Redis (node, service, event_type, severity, payload)ActionProposal— propozycja akcji z pełnymi metadanymi;.to_action_file()→ format executoraCooldownTracker— gate 5-minutowy persvc_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→ renameemit_event()— zapis zdarzeniaremediation_starteddo 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_name — PrintLogger nie ma atrybutu .name.
Zamiast tego łańcuch procesorów: add_log_level → TimeStamper → StackInfoRenderer
→ format_exc_info → JSONRenderer.
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:
- Dodano
extra_hosts: - "host-gateway:host-gateway"do docker-compose.yml .envużywaOLLAMA_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)