# Humanized Stepan v2 — Architecture Plan

**Версія:** 0.1-draft  
**Статус:** plan (без коду)  
**Область змін:** `crews/agromatrix_crew/` + мінімальний торкання `http_api.py`  
**Принцип:** fail-closed, backward-compatible, жодної нескінченної рекурсії

---

## 1. Проблеми поточної архітектури

| Симптом | Причина у коді |
|---------|----------------|
| На "привіт" запускаються всі 5 під-агентів | `run.py` завжди викликає ops, iot, platform, spreadsheet, sustainability |
| Роботизовані відповіді | JSON-схема фінального агента, відсутня адаптація стилю |
| Степан не знає хто ти | Немає UserProfile, жодного звернення до memory-service |
| Степан не знає твою ферму | Немає FarmProfile |
| Після відповіді немає самоперевірки | Reflection відсутній |
| Оператор і звичайний користувач мають однакову відповідь | is_operator є, але стиль не змінюється |
| Зміна `detect_intent()` ламає всю логіку | Ключові слова захардкожені в одній функції |

---

## 2. Загальна схема нового потоку

```
handle_message(text, user_id, chat_id, ops_mode)
    │
    ├─► [activation_gate.pre_check(text)]     ← блокує рекурсію, лічить глибину
    │
    ├─► [memory_manager.load(user_id)]        ← UserProfile + FarmProfile
    │         │ fallback: порожній профіль     ← fail-safe
    │
    ├─► [depth_classifier.classify(text, profile)]
    │         │ → DepthDecision {mode, intent, crew_needed, confidence}
    │         │ fallback: mode="deep"           ← fail-closed: краще зробити більше
    │
    ├─► if mode == "light":
    │       [style_adapter.render(profile)] → system_prompt_prefix
    │       Stepan відповідає сам (без під-агентів)
    │       → response
    │
    ├─► if mode == "deep":
    │       [activation_gate.select_crew(DepthDecision, FarmProfile)]
    │         → {ops?, iot?, platform?, spreadsheet?, sustainability?}
    │       Запускати ТІЛЬКИ потрібних під-агентів
    │       Stepan консолідує
    │       → response
    │
    ├─► [reflection_engine.reflect(response, profile, intent)]  ← один прохід, не рекурсія
    │         │ fallback: оригінальна відповідь
    │
    ├─► [memory_manager.update_async(user_id, text, response)] ← не блокує
    │
    └─► return final_response
```

---

## 3. Нові модулі

### 3.1 `depth_classifier.py`

**Розташування:** `crews/agromatrix_crew/depth_classifier.py`

**Відповідальність:** визначити глибину запиту і які под-агенти взагалі потрібні.

**Вхід:**
- `text: str` — текст повідомлення
- `profile: UserProfile | None` — профіль користувача
- `farm: FarmProfile | None` — профіль ферми

**Вихід: `DepthDecision`**
```python
@dataclass
class DepthDecision:
    mode: Literal["light", "deep"]   # ключовий перемикач
    intent: str                       # human-readable intent
    crew_needed: list[str]            # підмножина: ops, iot, platform, spreadsheet, sustainability
    confidence: float                 # 0..1, < 0.4 → force deep
    reason: str                       # для audit логу
```

**Логіка класифікації (rule-based, без LLM):**

Light mode — якщо текст відповідає хоча б одному патерну:
```
LIGHT_PATTERNS = {
    "greeting":      ["привіт", "доброго", "hello", "hi", "добрий ранок", "добрий вечір"],
    "thanks":        ["дякую", "дякуй", "спасибі", "дякую степан"],
    "ack":           ["зрозумів", "ок", "добре", "чудово", "зрозуміла"],
    "whoami_check":  ["хто я", "мої права"],
    "simple_status": ["який статус", "що зараз"],
}
```

Deep mode — якщо текст відповідає хоча б одному:
```
DEEP_PATTERNS = {
    "planning":      ["сплануй", "план на", "розробити план", "графік робіт"],
    "multi_ops":     ["по всіх полях", "кілька ділянок", "всі культури"],
    "iot_alert":     ["аномалія", "тривога", "sensors", "вологість впала"],
    "analysis":      ["план/факт", "план факт", "статистика", "зведення", "порівняй"],
    "decision":      ["що робити", "порадь", "проаналізуй", "виріши"],
    "recording":     ["запиши", "зафіксуй", "внеси", "додай операцію"],
}
```

Crew selection у deep mode:
```
crew_needed logic:
  "ops" → "запиши" | "зафіксуй" | "внеси" | farmos keywords
  "iot" → "датчик" | "вологість" | "temp" | "sensor" | FarmProfile.has_iot
  "platform" → "статус сервісів" | "інтеграція" | "помилка підключення"
  "spreadsheet" → "таблиц" | "excel" | "звіт" | "xlsx"
  "sustainability" → "зведення" | "агрегація" | "підсумки"
```

**Fail-safe:** будь-який виняток → `DepthDecision(mode="deep", intent="unknown", crew_needed=["ops","iot","platform","spreadsheet","sustainability"], confidence=0.0, reason="classifier_error")`.

---

### 3.2 `memory_manager.py`

**Розташування:** `crews/agromatrix_crew/memory_manager.py`

**Відповідальність:** завантажити, зберегти і оновити профілі через memory-service. Повна деградація до in-memory fallback.

**API:**
```python
def load(user_id: str) -> tuple[UserProfile, FarmProfile]
def update(user_id: str, interaction: InteractionContext) -> None
```

**Реалізація (sync, бо `run.py` sync):**
- HTTP запити через `httpx.Client` (sync), timeout 2s
- При недоступності memory-service → використовує `_local_cache: dict` (процесна пам'ять)
- `_local_cache` зберігає до 200 записів, TTL 30 хвилин
- Факт-ключі в memory-service:
  - `user_profile:agromatrix:{user_id}`
  - `farm_profile:agromatrix:{user_id}`
- user_id для memory-service: `stepan:{user_id}` (ізоляція від gateway-агентів)

**Fail-safe:**
```python
try:
    profile = _fetch_from_memory(user_id)
except Exception:
    profile = UserProfile.default(user_id)  # порожній, але валідний
    logger.warning("memory_manager: fallback to default profile user=%s", user_id)
```

**Не блокуючий update:**
```python
def update_async(user_id: str, interaction: InteractionContext):
    """Запускає оновлення в threading.Thread (daemon=True), не чекає результату."""
    t = threading.Thread(target=_do_update, args=(user_id, interaction), daemon=True)
    t.start()
```

---

### 3.3 `style_adapter.py`

**Розташування:** `crews/agromatrix_crew/style_adapter.py`

**Відповідальність:** сформувати prefix для system prompt Степана залежно від профілю.

**Вхід:** `UserProfile`, `DepthDecision`
**Вихід:** `str` — prefix для system prompt Степана

**Рівні expertise:**
```
novice:       мова проста, уникай термінів, давай короткий приклад, 2-3 речення
intermediate: збалансована відповідь, терміни пояснюй в дужках, до 5 речень
expert:       технічна відповідь, скорочений формат, опускай очевидне
```

**Стилі:**
```
brief:          1-2 речення, тільки суть
detailed:       повний опис з контекстом
conversational: живий тон, питання-відповідь, можна питати уточнення
```

**Формат prefix:**
```
"Відповідай на рівні {expertise_label}.
 Стиль: {style_label}.
 Ти знаєш цього користувача: {name or 'агрономе'}.
 Фермерський контекст: {farm_context_summary}."
```

**Fail-safe:** будь-який виняток → повертає порожній рядок, Степан працює зі стандартним backstory.

---

### 3.4 `reflection_engine.py`

**Розташування:** `crews/agromatrix_crew/reflection_engine.py`

**Відповідальність:** одноразова пост-обробка відповіді для відповідності профілю і стилю.

**Механізм (без LLM для Light mode, з LLM для Deep mode):**

**Light mode reflection (rule-based):**
- Відповідь > 500 символів і UserProfile.preferred_style == "brief" → обрізати до 3 речень
- Відповідь містить JSON-фрагменти → замінити на людський текст
- Відповідь містить технічні ідентифікатори (uuid, trace_id) → прибрати з відповіді користувачу

**Deep mode reflection (LLM, one-shot):**
```
Prompt:
"Оціни цю відповідь для {expertise_level} користувача:
[RESPONSE]
Якщо відповідь занадто технічна — спрости.
Якщо занадто довга для {preferred_style} — скороти.
Відповідай тільки виправленою відповіддю."
```

**Anti-recursion guard:**
```python
# В reflection_engine.py — module-level flag
_REFLECTING: bool = False

def reflect(response: str, profile: UserProfile, trace_id: str) -> str:
    global _REFLECTING
    if _REFLECTING:
        logger.warning("reflection: recursion guard active, skipping trace=%s", trace_id)
        return response
    _REFLECTING = True
    try:
        return _do_reflect(response, profile, trace_id)
    except Exception:
        return response
    finally:
        _REFLECTING = False
```

**Fail-safe:** будь-який виняток → повертає оригінальну відповідь без змін.

---

### 3.5 `activation_gate.py`

**Розташування:** `crews/agromatrix_crew/activation_gate.py`

**Відповідальність:**
1. Pre-check: блокує подвійний виклик handle_message з того самого контексту
2. Select: визначає мінімальний набір під-агентів для запуску
3. Post-check: обмежує глибину делегування

**Структура:**
```python
_CALL_DEPTH: threading.local  # per-thread, не глобальне

MAX_DEPTH = 1  # Степан може делегувати, але не можна повторно входити в handle_message

def pre_check(trace_id: str) -> bool:
    """Повертає True якщо дозволено продовжувати, False якщо глибина перевищена."""
    depth = getattr(_CALL_DEPTH, "depth", 0)
    if depth >= MAX_DEPTH:
        logger.error("activation_gate: max depth %d reached trace=%s", MAX_DEPTH, trace_id)
        return False
    _CALL_DEPTH.depth = depth + 1
    return True

def release(trace_id: str):
    """Зменшити лічильник після завершення handle_message."""
    _CALL_DEPTH.depth = max(0, getattr(_CALL_DEPTH, "depth", 0) - 1)

def select_crew(decision: DepthDecision, farm: FarmProfile) -> list[str]:
    """Повернути список під-агентів для запуску."""
    needed = list(decision.crew_needed)
    # Видалити IoT якщо FarmProfile.active_integrations не має iot
    if "iot" in needed and not farm.has_iot_integration:
        needed.remove("iot")
    # Видалити spreadsheet якщо не запит до таблиць
    if "spreadsheet" in needed and "spreadsheet" not in decision.intent:
        needed.remove("spreadsheet")
    return needed if needed else []
```

---

## 4. Структура UserProfile JSON

```json
{
  "_version": 1,
  "_fact_key": "user_profile:agromatrix:{user_id}",
  "user_id": "tg:123456789",
  "agent": "agromatrix",
  "name": "Іван",
  "expertise_level": "intermediate",
  "preferred_language": "uk",
  "preferred_style": "conversational",
  "last_seen": "2026-02-24T10:00:00Z",
  "interaction_count": 42,
  "known_intents": [
    "plan_day",
    "show_critical_tomorrow",
    "iot_status"
  ],
  "context_notes": [
    "has_farmos_access",
    "uses_thingsboard",
    "prefers_short_answers"
  ],
  "farm_profile_ref": "farm_profile:agromatrix:{user_id}",
  "recent_topics": [
    {"intent": "plan_day", "ts": "2026-02-24T09:00:00Z"},
    {"intent": "iot_status", "ts": "2026-02-23T18:00:00Z"}
  ],
  "operator": false,
  "updated_at": "2026-02-24T10:00:00Z"
}
```

**Поля та семантика:**

| Поле | Тип | Опис |
|------|-----|------|
| `expertise_level` | enum | novice / intermediate / expert; оновлюється автоматично після 10+ взаємодій |
| `preferred_style` | enum | brief / detailed / conversational |
| `interaction_count` | int | лічильник всіх взаємодій для авто-підвищення рівня |
| `known_intents` | list[str] | унікальні intents, накопичуються; use для FarmProfile автодоповнення |
| `context_notes` | list[str] | вільні мітки, збагачуються під час взаємодій |
| `recent_topics` | list[{intent, ts}] | останні 10 тем (для cold-start relief) |
| `operator` | bool | чи є цей user оператором (AGX_OPERATOR_IDS); read-only у memory |

---

## 5. Структура FarmProfile JSON

```json
{
  "_version": 1,
  "_fact_key": "farm_profile:agromatrix:{user_id}",
  "user_id": "tg:123456789",
  "farm_name": "Ферма Калинівка",
  "field_ids": ["field:north-01", "field:south-02"],
  "crop_ids": ["crop:wheat-winter", "crop:corn-hybrid"],
  "active_integrations": ["farmos", "thingsboard"],
  "seasonal_context": {
    "current_phase": "growing",
    "active_operations": ["irrigation", "monitoring"],
    "hemisphere": "north",
    "approximate_month": 2
  },
  "iot_sensors": {
    "has_iot_integration": true,
    "sensor_types": ["soil_moisture", "temperature"],
    "last_alert": null
  },
  "typical_intents": ["plan_day", "iot_status", "plan_vs_fact"],
  "alert_thresholds": {
    "soil_moisture_min": 20.0,
    "temperature_min": -5.0,
    "temperature_max": 38.0
  },
  "dict_pending_count": 0,
  "updated_at": "2026-02-24T10:00:00Z"
}
```

**Поля та семантика:**

| Поле | Тип | Опис |
|------|-----|------|
| `field_ids` | list[str] | заповнюються під час нормалізації терміну tool_dictionary |
| `crop_ids` | list[str] | аналогічно |
| `active_integrations` | list[str] | визначають які crew_agents потенційно потрібні |
| `seasonal_context` | object | підказки для планування і класифікатора глибини |
| `iot_sensors.has_iot_integration` | bool | ключ для activation_gate: чи включати IoT агента |
| `typical_intents` | list[str] | акумулюються; використовуються для Light/Deep розмежування |
| `dict_pending_count` | int | кеш кількості pending термінів для оператора |
| `alert_thresholds` | object | якщо IoT дані виходять за поріг → auto-trigger Deep mode |

---

## 6. Коли і як оновлюється профіль

### UserProfile

| Подія | Що оновлюється | Коли |
|-------|----------------|------|
| Будь-яка взаємодія | `last_seen`, `interaction_count`, `recent_topics` | Завжди, після відповіді |
| Новий intent | `known_intents.append(intent)` | Якщо intent не порожній |
| interaction_count >= 10 і всі intents — "planning" | `expertise_level` → intermediate | При update |
| interaction_count >= 30 і є технічні intents | `expertise_level` → expert | При update |
| Оператор надіслав `/profile set style brief` | `preferred_style` | Одразу |
| FarmProfile змінений | `farm_profile_ref` sync | При update |

### FarmProfile

| Подія | Що оновлюється | Коли |
|-------|----------------|------|
| tool_dictionary.normalize успішний | `field_ids`, `crop_ids` | При нормалізації |
| Новий інтент з IoT | `active_integrations`, `iot_sensors.has_iot_integration` | При Deep mode |
| Новий інтент з spreadsheet | `active_integrations.append("spreadsheet")` | При Deep mode |
| Оператор `/farm update phase=sowing` | `seasonal_context.current_phase` | Одразу |
| dict_review.stats() | `dict_pending_count` | При ops_mode load |

---

## 7. Тригери Deep mode

**Автоматичні (depth_classifier):**

| Тригер | Умова |
|--------|-------|
| Планування | текст містить DEEP_PATTERNS["planning"] |
| Мультипольова операція | DEEP_PATTERNS["multi_ops"] |
| IoT аномалія | DEEP_PATTERNS["iot_alert"] АБО IoT дані з alert_thresholds порушені |
| Аналіз план/факт | DEEP_PATTERNS["analysis"] |
| Запис у farmOS | DEEP_PATTERNS["recording"] |
| Низька впевненість | confidence < 0.4 після класифікації |
| Нові терміни | tool_dictionary normalization повернув pending items |
| Перша взаємодія | interaction_count == 0 (невідомий користувач) |

**Примусові (env/flag):**

| Тригер | Механізм |
|--------|----------|
| `AGX_FORCE_DEEP=1` | env в контейнері (тестування) |
| Текст починається з `--deep` | парситься в handle_message before classify |
| Оператор вручну | operator_commands + flag в trace |

---

## 8. Тригери запуску під-команди (активація crew_agent)

| Crew Agent | Тригер (keyword or FarmProfile) | Light може обійтись? |
|------------|----------------------------------|----------------------|
| `ops` | "запиши", "внеси", "зафіксуй", "farmOS" | Ні |
| `iot` | "датчик", "вологість", "температура" + `has_iot_integration=true` | Ні |
| `platform` | "статус", "перевір сервіс", "інтеграція впала" | Іноді (кешований статус) |
| `spreadsheet` | "таблиця", "excel", "звіт", "xlsx" | Ні |
| `sustainability` | "зведення", "агрегація", "підсумки по сезону" | Ні |
| **всі одночасно** | `intent == "general"` без профілю (fallback) | Ні |

---

## 9. Ситуації, що залишаються Light mode

| Ситуація | Чому Light | Хто відповідає |
|----------|------------|----------------|
| Привітання будь-якого типу | Не потребує даних з farmOS/IoT | Степан з style_adapter |
| "Дякую", "ок", "зрозумів" | Підтвердження, не запит | Степан (2 слова) |
| /whoami, /pending, /approve | Operator commands | operator_commands.py (незмінний) |
| "Що ти вмієш?" | Довідка | Степан з профілем |
| Повторне питання тієї ж теми (< 5 хв) | recent_topics cache | Степан з кешем контексту |
| Simple status якщо кеш свіжий | FarmProfile.seasonal_context свіжий (< 1 год) | Степан без crew |
| Повідомлення < 4 слів | Незрозумілий запит → уточнення | Степан питає |
| Текст не пов'язаний з агрономією | Off-topic filter | Степан ввічливо redirects |

---

## 10. Принцип fail-safe

**Ієрархія деградації:**

```
Нормальна робота:
    memory-service online → профілі загружені → класифікатор → вибір crew → рефлексія

Деградація 1 (memory недоступна):
    fallback UserProfile.default() → класифікатор без персоналізації → crew → рефлексія skip

Деградація 2 (classifier помилка):
    force Deep mode → всі crew → рефлексія skip

Деградація 3 (частина crew агентів впала):
    інші crew продовжують → Степан синтезує з частковими даними
    run_task_with_retry вже існує (max_retries=2)

Деградація 4 (OpenAI недоступний):
    handle_stepan_message повертає "Помилка обробки. trace_id=..."
    gateway вже обробляє це (stepan_disabled fallback)
```

**Правила:**
- Жодний модуль не може кинути виняток, що зупинить `handle_message`
- Кожен новий модуль wrap-ується в try/except з fallback
- `reflection_engine` завжди має повертати `str`, ніколи `None` або виняток
- `memory_manager.update_async` daemon=True — смерть процесу не втрачає відповідь
- При будь-якій помилці profile: `interaction_count=0`, `expertise_level="intermediate"`, `preferred_style="conversational"`

---

## 11. Як не створити нескінченну рекурсію

**Три незалежні шари захисту:**

### Шар 1 — `activation_gate` (threading.local counter)
```
handle_message:
  pre_check() → depth becomes 1
  ... робота ...
  release()   → depth back to 0

Якщо under_running_task викликає handle_message:
  pre_check() → depth == 1 → MAX_DEPTH reached → return error response
```
`threading.local` — ізоляція per-thread, не заважає паралельним викликам з різних чатів.

### Шар 2 — `reflection_engine._REFLECTING` flag
- Глобальний (module-level) булевий прапорець
- Встановлюється в `True` перед LLM-рефлексією, скидається в `finally`
- Якщо рефлексія викличе щось що знову зайде в рефлексію → миттєво скидається

### Шар 3 — Архітектурна заборона
- Під-агенти (ops, iot, platform, spreadsheet, sustainability) мають `allow_delegation=False`
- Жоден агент не має знань про `handle_message` або `run.py`
- `depth_classifier`, `style_adapter`, `memory_manager` — pure functions, без CrewAI, без LLM
- Тільки `reflection_engine` (Deep mode) і фінальна задача Степана — LLM-виклики

---

## 12. Де саме інтегрувати

### 12.1 `crews/agromatrix_crew/run.py`

**Змінити:**
```python
# Новий imports (top)
from crews.agromatrix_crew.depth_classifier import classify, DepthDecision
from crews.agromatrix_crew.memory_manager import load_profiles, update_async
from crews.agromatrix_crew.style_adapter import build_prefix
from crews.agromatrix_crew.reflection_engine import reflect
from crews.agromatrix_crew.activation_gate import pre_check, release, select_crew

# handle_message:
# 1. pre_check (перше, до всього)
# 2. load_profiles (до classify)
# 3. classify (до побудови агентів)
# 4. if light → stepan_only_response
# 5. if deep → activation_gate.select_crew → run selected
# 6. reflect (після відповіді)
# 7. update_async (не блокуючий, daemon thread)
# 8. release (в finally)
```

**Зберегти:**
- Весь `route_operator_command` / `route_operator_text` (operator_commands не змінюємо)
- `tool_dictionary.normalize_from_text` + pending check (залишається до classify)
- `run_task_with_retry` (залишається для Deep mode)
- `audit_event` (залишається, розширюємо depth/mode в event)
- `farmos_ui_hint` (залишається)

**НЕ змінювати:**
- Сигнатуру `handle_message(text, user_id, chat_id, trace_id, ops_mode, last_pending_list)`
- Формат повернення (str, valid for JSON parse by http_api)

### 12.2 `crews/agromatrix_crew/operator_commands.py`

**Додати команди:**
```
/profile          → показати UserProfile (user_id, expertise, style, last_seen, interaction_count)
/profile set <k>=<v>  → оновити expertise_level або preferred_style
/farm             → показати FarmProfile (коротко: поля, культури, інтеграції, сезон)
/farm update <k>=<v>  → оновити seasonal_context.current_phase, порогові значення
```

**Зберегти без змін:**
- `/whoami`, `/pending`, `/approve`, `/reject`, `/apply_dict`, `/pending_stats`
- `is_operator()` — не змінювати
- `route_operator_command()` — розширити case, не переписувати
- `route_operator_text()` — залишити

**OPERATOR_COMMANDS set** — додати `"profile"`, `"farm"`.

### 12.3 `gateway-bot/http_api.py`

**Мінімальні зміни:**
- Додати env `AGX_FORCE_DEEP` → якщо "1", передавати в metadata або через handle_message (ops_mode вже є, можна додати depth_override parameter)
- **Нічого більше не змінювати.** handle_message вже приймає text, user_id, chat_id, trace_id, ops_mode.

**Не змінювати:**
- Маршрутизацію оператор/не-оператор (вже виправлена попереднім патчем)
- STEPAN_IMPORTS_OK logic
- doc_context logic

### 12.4 `memory-service`

**Не змінювати сервіс.** Використовуємо існуючий `/facts/upsert` і `/facts/get`.

**Нові fact-ключі:**
```
user_profile:agromatrix:{user_id}   → UserProfile JSON (fact_value_json)
farm_profile:agromatrix:{user_id}   → FarmProfile JSON (fact_value_json)
```

**memory_manager.py в crews** викликає memory-service по HTTP (sync httpx), URL з env:
```
AGX_MEMORY_SERVICE_URL=http://memory-service:8000
```

---

## 13. Схема файлів після впровадження

```
crews/agromatrix_crew/
├── __init__.py
├── run.py                        ← ЗМІНЕНО (нові модулі вмонтовані)
├── audit.py                      ← без змін
├── operator_commands.py          ← РОЗШИРЕНО (/profile, /farm)
│
├── depth_classifier.py           ← НОВИЙ
├── memory_manager.py             ← НОВИЙ
├── style_adapter.py              ← НОВИЙ
├── reflection_engine.py          ← НОВИЙ
├── activation_gate.py            ← НОВИЙ
│
├── agents/
│   ├── stepan_orchestrator.py    ← backstory розширюється від style_adapter
│   ├── operations_agent.py       ← без змін
│   ├── iot_agent.py              ← без змін
│   ├── platform_agent.py         ← без змін
│   ├── spreadsheet_agent.py      ← без змін
│   └── sustainability_agent.py   ← без змін
│
├── tasks/
│   ├── intake_and_plan.py        ← без змін (лише для compatibility)
│   ├── execute_ops.py            ← без змін
│   ├── execute_iot.py            ← без змін
│   ├── execute_spreadsheets.py   ← без змін
│   └── reporting.py              ← без змін
│
└── tools/
    └── __init__.py               ← без змін
```

---

## 14. Порядок впровадження (поетапно)

**Фаза 1 — Foundation (без змін у run.py)**
1. `memory_manager.py` — реалізувати, написати unit-тест з mock memory-service
2. `depth_classifier.py` — реалізувати rule-based, написати тести по кожному патерну
3. `activation_gate.py` — реалізувати pre_check/release/select_crew, тест на рекурсію

**Фаза 2 — Light mode**
4. `style_adapter.py` — реалізувати три рівні і три стилі
5. Модифікувати `run.py`: вставити Light mode path (якщо light → пропустити всі crew)
6. Smoke-test: надіслати "привіт" → відповідь без crew

**Фаза 3 — Deep mode + Activation Gate**
7. Модифікувати `run.py`: Deep mode використовує `select_crew`, не всіх 5 агентів
8. Тест: `"сплануй тиждень"` → ops + sustainability, але не iot (якщо has_iot=false)

**Фаза 4 — Reflection + Profiles**
9. `reflection_engine.py` — rule-based Light reflection (без LLM)
10. Оновити `operator_commands.py` — `/profile`, `/farm`
11. E2E тест: 3 взаємодії → перевірка UserProfile накопичення

**Фаза 5 — Deep reflection (LLM)**
12. Додати LLM-рефлексію тільки для Deep mode
13. Тест на рекурсію: перевірити `_REFLECTING` flag спрацьовує

---

## 15. Метрики успіху

| Метрика | Ціль |
|---------|------|
| % запитів у Light mode (грітинги + прості) | > 30% від загального трафіку |
| Середній час відповіді Light mode | < 2s (без crew launch) |
| Середній час відповіді Deep mode | < 30s (тільки потрібні crew) |
| % запитів що запускають тільки 1-2 crew | > 50% від Deep запитів |
| Оператор `/profile` — відображає дані | 100% (якщо memory-service online) |
| Fallback без memory-service | Gateway не падає (fail-safe) |
| Рекурсивний виклик handle_message | 0 (activation_gate блокує) |

---

## 16. Відкриті питання (потрібно вирішити перед реалізацією)

1. **Sync vs async memory_manager**: `run.py` sync, але memory-service async-HTTP. Поточне рішення — sync httpx.Client. Альтернатива: asyncio.run() в окремому thread. Потребує рішення.
2. **UserProfile.expertise_level auto-upgrade**: поріг 10/30 взаємодій — достатньо? Або враховувати час між взаємодіями?
3. **reflection LLM model**: який LLM для рефлексії — той самий GPT-4, або дешевший GPT-3.5/Mistral? Вплив на latency та cost.
4. **FarmProfile cold-start**: перша взаємодія — profile порожній. Deep mode завжди? Або запитати у користувача дані ферми?
5. **Multi-user farm**: кілька операторів з однієї ферми — один FarmProfile чи кілька? Зараз `user_id`-based.
6. **Operator profile isolation**: оператор і звичайний користувач можуть мати одне user_id якщо оператор пише без оператор-чату. Чи потрібна окрема UserProfile для ops-mode?

---

*Документ готовий до review. Після погодження — розпочинати Фазу 1.*
