simbiot-harness/README.md
Simbiot Harness 76aa204f2b feat(tg-cmd): без-LLM команды бота /лестница /статус /задачи /помощь — мгновенные ответы сек-рефлексом + setMyCommands (t-260929210755-9bf6a6)
- harness/tg_commands.py: реестр команд, парсер (кириллица/транслит/@бот/без слэша),
  хендлеры из read model, answer_pending (идемпотентно, cooldown 60с при лежащем TG),
  setMyCommands с фолбэком кириллица→транслит (Bot API латиницу только)
- scheduler.py: сек-тик отвечает на команды без LLM (payload.commands),
  build_context НЕ показывает команды мин-уровню (гонки/лежащий TG не тратят LLM)
- tg_inbox.py: send_message/send_reply принимают короткий timeout для рефлекса;
  поллер при старте авторегистрирует команды бота
- cli.py: tg-commands --register/--list
2026-09-30 02:30:27 +00:00

137 lines
No EOL
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Simbiot Harness
Ритмичный агент: живёт тактами, как человек. CQRS + Event Sourcing на спине,
Strands Harness SDK как исполнительное ядро, модели через 9router.
## Лестница (v0.1)
| Уровень | Интервал | Контекст | Модель | Задачи |
|---|---|---|---|---|
| sec | 1 с | 0 (без LLM) | — | рефлекс: телеметрия, алерты |
| min | 1 мин | ~4K | auto→flash | S-триаж: входящие, быстрые фиксы |
| hour | 1 ч | ~32K | auto | S/M-задачи |
| day | 1 д | ~128K | auto | M/L-задачи, манифест дня |
Принципы: тик идемпотентен (`UNIQUE(level, seq)`); занятый уровень поглощает
тик (`TickAbsorbed` + детерминированная заглушка дайджеста, без LLM);
запущенная задача не прерывается; наверх идут только дайджесты.
## События (ES-спина)
`CycleTicked`, `TickAbsorbed`, `ReflexObserved`, `DigestWritten`,
`TaskSubmitted`, `TaskClaimed`, `TaskCompleted`, `TaskFailed`, `TaskPromoted`,
`TaskAwaitDelivery`, `DeliverySent`, `EscalationRaised`,
`EscalationAcknowledged`, `BudgetSpent`, `CycleError`, `HarnessInit`,
`PolicySet` — append-only, проекции пересобираются replay'ем, стейт =
производное от событий.
## Структура
- `harness/es.py` — event store + проекции (checkpoint + replay)
- `harness/levels.py` — конфигурация уровней
- `harness/scheduler.py` — демон лестницы
- `harness/isolated_runner.py` + `_llm_worker.py` — LLM-тик в сабпроцессе
(глобальный asyncio-рантайм Strands не переживает 2+ `create_harness`)
- `harness/commands.py` — command side
- `harness/tg_commands.py` — без-LLM команды бота (сек-рефлекс)
- `cli.py` — init/task/task-await/tasks/status/events/requeue
## CLI
```bash
./venv/bin/python cli.py init --identity "…"
./venv/bin/python cli.py task "…" -m 240 # -m minutes-to-live estimate: S/M/L
./venv/bin/python cli.py task "…" -a # автодоставка результата в TG при готовности
./venv/bin/python cli.py task-await <task-id> # пометить существующую: доставить в TG
./venv/bin/python cli.py tasks
./venv/bin/python cli.py status
./venv/bin/python cli.py events -n 50
./venv/bin/python cli.py requeue <task-id> -r "причина"
./venv/bin/python cli.py promote <task-id> -r "вне класса уровня" # S→M→L
./venv/bin/python cli.py escalate "причина" -f min -c "контекст" -s critical
./venv/bin/python cli.py escalations --open # открытые эскалации
./venv/bin/python cli.py escalation-ack <esc-id> -n "что сделал" # закрыть
./venv/bin/python cli.py tg-reply <msg-id> "текст" # ответ на входящее TG
```
## Автодоставка артефактов (t-260929205927)
Задача, заказанная из TG (`cli.py task -a` / `task-await`), помечается
`TaskAwaitDelivery`. При завершении (`TaskCompleted`) результат (answer)
уходит в чат сам — `DeliverySent`, без повторного запроса. Push в тике
(сразу после завершения) + pull-страховка (delivery-демон каждые 15с:
досылает недоставленное, если тик умер между завершением и отправкой).
Идемпотентно: один раз доставлено — повторно не шлётся; после requeue
доработка доставляется снова. Формат: «✅ Готово: <title>» + выжимка ≤1800с.
## Эскалации
Важное с нижних уровней поднимается наверх событием `EscalationRaised`
(min→hour→day; day — вершина, эскалировать некуда). Открытые эскалации
инжектятся в контекст верхних уровней (`build_context`) в приоритетной секции
— нижний уровень не тонет в чужих проблемах, верхний видит выжимку, а не сырь.
Уровень-адресат закрывает эскалацию `escalation-ack` (событие
`EscalationAcknowledged`). Падение LLM-тика (`CycleError`) эскалируется
автоматически — scheduler поднимает warn-эскалацию на уровень выше.
## Запуск
```bash
systemctl enable --now simbiot-harness
journalctl -u simbiot-harness -f
```
## Telegram-инбокс
Бот @juno42bot → long polling (`simbiot-harness-tg.service`) →
`MessageReceived` в стор → минутный уровень видит входящие в контексте →
отвечает `cli.py tg-reply` (Telegram-отправка + `ReplySent` закрывает входящее).
Белый список чатов в `.env` (`SIMBIOT_TG_ALLOWED`), чужие → `MessageIgnored`.
## Без-LLM команды бота (t-260929210755)
`/лестница` `/статус` `/задачи` `/помощь` — мгновенные ответы из read model:
их закрывает **секундный рефлекс** (`harness/tg_commands.py` в `_reflex_tick`),
минутный LLM-контекст на команды не тратится. Алиасы (латиница/транслит,
`/cmd@bot`, одно слово без слэша) понимает парсер; список в чате —
`setMyCommands` (кириллица, фолбэк — транслит), регистрируется стартом
tg-поллера или вручную:
```bash
./venv/bin/python cli.py tg-commands --register # setMyCommands
./venv/bin/python cli.py tg-commands --list # что у бота сейчас
```
События: `CommandHandled` (команда закрыта рефлексом) + `ReplySent`;
`build_context` не показывает команды мин-уровню — даже без ответа
(гонка на границе тика / лежащий TG не заставят минуту тратить LLM).
## Дашборд задач
Веб-страница со всеми задачами (статусы/размеры/уровни) + лестница циклов +
эскалации + свежие дайджесты, автообновление каждые 5с (JSON-API `/api/state`).
```bash
systemctl status simbiot-dashboard # порт 8765, healthcheck 8766
TOKEN=$(grep -oP '(?<=SIMBIOT_DASH_TOKEN=).*' /etc/simbiot/dashboard.env)
xdg-open "http://<host>:8765/?token=$TOKEN" # токен пишется в cookie
curl "http://127.0.0.1:8765/api/state?token=$TOKEN" | jq . # машиночитаемо
```
Реализация: `harness/dashboard.py` (stdlib-only http.server, read-only из
event store через `run_projections()` — консистентно с scheduler/CLI) +
`harness/dashboard.html`. Авторизация: `SIMBIOT_DASH_TOKEN` из
`/etc/simbiot/dashboard.env` (env-файл юнита, режим 600); без токена — 401.
UFW: порт 8765/tcp открыт (токен-защищён).
## Модели
9router (router.cil.by), отдельный ключ `sk-simbiot` в `.env`, комбо `auto`:
glm-5.3 → kimi-k3 → deepseek-v4-pro → v4.1-flash → flash. Бюджет виден
в событиях `BudgetSpent` (по уровням) и в дашборде 9router по ключу.
## v0.2 (roadmap)
- прерывание срочной реакцией / пауза (сейчас absorbed детерминирован)
- уровни week/month/quarter/year
- инбокс из Telegram/портала