RAG (t-260929212316): каждому LLM-тику доступен rag_lookup — семантический поиск по базе кооператива (/root/rag, fastembed+sqlite). Лимиты зашиты в инструмент: 1 запрос/тик, k<=5, таймаут 20с, сабпроцесс rag-venv, мёртвый вендор не валит тик. Трейс RagQueried в сторе. CLI: rag-query. Хард-кап шагов (e-260930031800): мин-воркер тонул в 90+ tool-вызовах и умирал по таймауту 300с. До: 3 CycleError за 2 часа, эскалации min→hour. Фикс: cancel_tool на вызовы сверх лимита + end_turn (AfterToolsEvent) останавливает цикл после партии + limits.turns (нативный механизм SDK). e2e: 20 параллельных вызовов при лимите 3 → ровно 3 исполнения, цикл остановлен, тик завершён текстом за ~12с (было: таймаут 300с). Мандат мин-уровня: про rag_lookup и приоритет RAG для вопросов про кооператив. Тесты: tests/test_rag_lookup.py (29 PASS), tests/test_tool_budget.py (12 PASS).
158 lines
No EOL
9.6 KiB
Markdown
158 lines
No EOL
9.6 KiB
Markdown
# 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).
|
||
|
||
## RAG в тике (t-260929212316)
|
||
|
||
Каждый LLM-тик получает инструмент `rag_lookup` — семантический поиск
|
||
по базе знаний кооператива (вендор `/root/rag`: fastembed bge-small +
|
||
sqlite, rag.db ~12.5k чанков по контексту/навыкам, hermes.db ~54k).
|
||
Лимиты зашиты в инструмент, не в промпт: **1 запрос на тик**, k<=5,
|
||
таймаут 20с, сабпроцесс `rag-venv` (ONNX-модель не грузится в процесс
|
||
воркера), упавший вендор не валит тик. Трейс — `RagQueried` в сторе.
|
||
Ручной вызов: `cli.py rag-query "вопрос" -k 5 [--db ...] [--json]`.
|
||
Выключить: `RAG_DISABLED=1`.
|
||
|
||
## Хард-кап шагов тика (e-260930031800)
|
||
|
||
Мин-воркер уходил в исследовательские цепочки (90+ tool-вызовов) и умирал
|
||
по таймауту 300с — CycleError, эскалации min→hour каждую ~50 минут.
|
||
Защита в `_llm_worker`: `BeforeToolCallEvent.cancel_tool` отменяет
|
||
вызова сверх лимита, `AfterToolsEvent.end_turn` останавливает цикл
|
||
агента ПОСЛЕ партии инструментов (без него модель шлёт шаги снова —
|
||
e2e: 20 отмен подряд), `limits={"turns": N}` — нативный кап ходов SDK.
|
||
Лимиты: `SIMBIOT_MAX_TOOL_CALLS` (по умолчанию 30, счёт по реальным
|
||
исполнениям), `SIMBIOT_MAX_TURNS` (по умолчанию 15).
|
||
|
||
## Дашборд задач
|
||
|
||
Веб-страница со всеми задачами (статусы/размеры/уровни) + лестница циклов +
|
||
эскалации + свежие дайджесты, автообновление каждые 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/портала |