405 lines
No EOL
29 KiB
Markdown
405 lines
No EOL
29 KiB
Markdown
# Simbiot Harness
|
||
|
||
Ритмичный агент: живёт тактами, как человек. CQRS + Event Sourcing на спине,
|
||
Strands Harness SDK как исполнительное ядро, модели через 9router.
|
||
|
||
## Установка одной командой (t-261002193628-1d1e5f, как hermes)
|
||
|
||
```bash
|
||
curl -fsSL https://raw.cil.by/simbiot/install.sh | bash
|
||
```
|
||
|
||
Что делает `scripts/install.sh`: клонирует репо в `~/simbiot-harness`,
|
||
создаёт venv (Python 3.11+), ставит зависимости, кладёт обёртку
|
||
`~/.local/bin/simbiot` (чистый env: unset PYTHONPATH/PYTHONHOME →
|
||
`exec venv/bin/python cli.py`), тихо инициализирует стор и проверяет
|
||
`simbiot status`. Флаги: `--dir DIR`, `--repo URL`, `--wizard` /
|
||
`--no-wizard`, `--systemd` (юнит, только root), `-q` (тихо).
|
||
|
||
Если в каталоге установки остался битый venv (python без pip — частый кейс
|
||
WSL/старых клонов), скрипт пересоздаёт его и ставит зависимости (при
|
||
недоступном ensurepip — системным pip3 в site-packages venv). В конце
|
||
установки предлагается следующий шаг: портал https://cil.by — залогиниться
|
||
и взять ключ API, затем `simbiot wizard --router-key <КЛЮЧ>`.
|
||
|
||
Первый запуск — визард настройки (токен TG-бота, ключ 9router, модель):
|
||
|
||
```bash
|
||
simbiot wizard # интерактивно
|
||
simbiot wizard --noninteractive --router-key KEY --model auto
|
||
```
|
||
|
||
`.env` в корне репо читается стартапом (SIMBIOT_TG_TOKEN,
|
||
SIMBIOT_TG_ALLOWED, ROUTER_KEY, SIMBIOT_MODEL и др.); в systemd-режиме
|
||
`scripts/simbiot-harness.service` подхватывает его сам.
|
||
|
||
## Главная цель (фиксация Alex, 2026-09-30)
|
||
|
||
Метрика харнеса — **максимизация долгосрочной AUM** (доход/ресурс всех
|
||
проектов кооператива в длинном горизонте). Главный вопрос к любому уровню
|
||
и любой задаче: «добавит ≥1% к AUM в горизонте год+?» — что приносит
|
||
короткий доход, но съедает долгосрочную базу, приоритет ниже. На этой
|
||
цели стоит автоприоритизация доски задач (t-260929161138; уточнение
|
||
автоприоритизации день+час — t-260930153845, задача e-260930161330).
|
||
|
||
Механика (t-260929161138, docs/aum.md): `cli.py aum --record` — снапшот
|
||
метрики (AUM из docs/aum/balances.json + burn 9router за сутки/неделю) →
|
||
событие `AumMeasured` → `policy.aum_target` → «ЦЕЛЬ (AUM)» инжектится в
|
||
контекст каждого тика. Пилот петли оптимизации (симуляции + генетический
|
||
отбор конфигураций burn, отчёт-адвокат): `cli.py aum-pilot` — песочница,
|
||
без авто-применения.
|
||
|
||
## Криптоаудит (t-260930083716)
|
||
|
||
Каждое событие подписывается при записи приватным ключом харнеса
|
||
(ed25519, `.audit/ed25519.key` 0600), события сцеплены по `prev_sig`
|
||
(тампер-цепочка); команды CLI — конверты `audit-command`/`verify-command`;
|
||
проекции отвергают подделанный хвост, полный аудит — `cli.py verify`
|
||
(выход 0/2), публичный ключ — `cli.py audit-key` / `.audit/ed25519.pub`.
|
||
Подробно: docs/audit.md.
|
||
|
||
## Лестница (v0.1)
|
||
|
||
| Уровень | Интервал | Контекст | Модель | Задачи |
|
||
|---|---|---|---|---|
|
||
| sec | 1 с | 0 (без LLM) | — | рефлекс: телеметрия, алерты |
|
||
| min | 1 мин | 256K | auto (effort low) | S-триаж: входящие, быстрые фиксы |
|
||
| hour | 1 ч | 512K | auto (medium) | S/M-задачи, дайджест часа |
|
||
| day | 1 д | 1M | auto (high) | M/L-задачи, манифест дня |
|
||
|
||
Конфиг согласован Алексом (e-261001082710): min 256k → hour 512k → day 1M
|
||
(ctx_tokens в `harness/levels.py`); week/month/quarter/year — в roadmap v0.2.
|
||
|
||
Принципы: тик идемпотентен (`UNIQUE(level, seq)`); занятый уровень поглощает
|
||
тик (`TickAbsorbed` + детерминированная заглушка дайджеста, без LLM);
|
||
запущенная задача не прерывается; наверх идут только дайджесты.
|
||
|
||
## События (ES-спина)
|
||
|
||
`CycleTicked`, `TickAbsorbed`, `ReflexObserved`, `DigestWritten`,
|
||
`TaskSubmitted`, `TaskClaimed`, `TaskCompleted`, `TaskFailed`, `TaskPromoted`,
|
||
`TaskAwaitDelivery`, `DeliverySent`, `InitiativeSent`, `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 команды бота (сек-рефлекс)
|
||
- `harness/paths.py` — карта персистентных путей (SIMBIOT_HOME)
|
||
- `cli.py` — init/task/task-await/tasks/status/events/requeue
|
||
|
||
## Изоляция памяти инстанса — SIMBIOT_HOME (t-261004113351)
|
||
|
||
Память агента — данные, не код: при переносе инстанса другому владельцу
|
||
(мультитенантность) он не должен «думать» чужими проектами. Всё, что живёт
|
||
дольше процесса — ES-база, RAG-индекс знаний, `.env`, ключи аудита, выжимки,
|
||
штампы каналов, доки инстанса — размещается в одном каталоге-хоме, репо
|
||
остаётся чистым кодом. Свежие установки: `~/.simbiot-data` (install.sh
|
||
проставляет в обёртку и systemd-юнит автоматически).
|
||
|
||
```
|
||
SIMBIOT_HOME=/path/to/home venv/bin/python cli.py status
|
||
```
|
||
|
||
| файл | назначение |
|
||
|---|---|
|
||
| `<home>/db/harness.db` | ES-база (события, проекции, delivery-леджер) |
|
||
| `<home>/rag/rag.db` | RAG-индекс знаний инстанса |
|
||
| `<home>/env/.env` | секреты (токены, ключи роутера), 0600 |
|
||
| `<home>/docs/` | проектные документы владельца (aum/balances.json) |
|
||
| `<home>/digests/` | файлы-выжимки уровней |
|
||
| `<home>/audit/` | ключи крипто-аудита ed25519 |
|
||
| `<home>/inbox/`, `<home>/run/`, `<home>/voice/` | каналы, штампы, кэш ASR |
|
||
|
||
Приоритет: точечная env (`SIMBIOT_DB`, `SIMBIOT_RAG_DB`, …) → `SIMBIOT_HOME`
|
||
→ legacy-пути из корня репо (обратная совместимость: не задан `SIMBIOT_HOME`
|
||
— всё работает по-старому).
|
||
|
||
Новый инстанс (например, для Юры): `scripts/init_instance.py` — чистая база +
|
||
идентичность владельца + его доки (+RAG-индексация) + опциональный селективный
|
||
импорт выжимок/доков из инстанса-источника. Тест изоляции:
|
||
`tests/test_instance_isolation.py` (поднять копию с пустой памятью — контекст
|
||
тика и RAG не содержат чужих проектов, 14 проверок). Перенос существующего
|
||
инстанса в хом: `scripts/migrate_to_home.sh` (ждёт тихое окно без LLM-воркеров,
|
||
идемпотентен).
|
||
|
||
## 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-260930064924): один сводный ответ закрывает несколько входящих
|
||
./venv/bin/python cli.py tg-reply m-1,m-2,m-3 -c "текст" # ReplySent на каждое, одна отправка
|
||
# идемпотентные уведомления/файлы (t-260930083937): повтор — SKIP, дублей в чат нет
|
||
./venv/bin/python cli.py tg-notify "текст" -k <ключ>
|
||
./venv/bin/python cli.py tg-deliver-file <путь> -c "подпись" [--force]
|
||
```
|
||
|
||
## Автодоставка артефактов (t-260929205927)
|
||
|
||
Задача, заказанная из TG (`cli.py task -a` / `task-await`), помечается
|
||
`TaskAwaitDelivery`. При завершении (`TaskCompleted`) результат (answer)
|
||
уходит в чат сам — `DeliverySent`, без повторного запроса. Push в тике
|
||
(сразу после завершения) + pull-страховка (delivery-демон каждые 15с:
|
||
досылает недоставленное, если тик умер между завершением и отправкой).
|
||
Идемпотентно: один раз доставлено — повторно не шлётся; после requeue
|
||
доработка доставляется снова. Формат: «✅ Готово: <title>» + выжимка ≤1800с.
|
||
|
||
## Идемпотентность доставки (t-260930083937)
|
||
|
||
Инцидент: Alex получал презентацию трижды (m-516173239/241) — гонка
|
||
push (тик) vs pull (демон): оба проходили проверку `delivery_status != 'sent'`,
|
||
пока TG-отправка (до 35с) была в полёте; плюс ad-hoc curl sendDocument без
|
||
метки delivered. Фикс — три атомарных барьера в `deliver_result` /
|
||
`deliver_artifact`:
|
||
|
||
1. **ledger** (`delivery_ledger`, INSERT OR IGNORE): факт «доставлено раз и
|
||
навсегда» по артефакт-идентификатору (задача/файл/ключ сообщения).
|
||
Повтор → SKIP + событие `DeliverySkipped` (пропуск логируется);
|
||
2. **claim** (`delivery_locks`, атомарный INSERT…WHERE NOT EXISTS):
|
||
пометка «доставляю» ДО отправки, TTL 120с > таймаута TG. Параллельный
|
||
уровень/демон получает False и пропускает — гонка закрыта на уровне
|
||
SQLite (межпроцессно, WAL). Сбой отправки → release, демон досыплет;
|
||
3. **запись в ledger** ПОСЛЕ успеха + `DeliverySent`.
|
||
|
||
Requeue сбрасывает ledger — доработка доставляется заново. Файлы в чат —
|
||
только через `cli.py tg-deliver-file` (тот же барьер); прямые
|
||
curl/api.telegram отправки из тиков запрещены (мандаты уровней).
|
||
Тесты: `tests/test_delivery_idempotency.py` (49 проверок: гонки, сбои,
|
||
requeue, межпроцессный claim, CLI).
|
||
|
||
## Эскалации
|
||
|
||
Важное с нижних уровней поднимается наверх событием `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`.
|
||
|
||
Батч-триаж (t-260930064924): минутный тик видит ВСЕ неотвеченные сообщения
|
||
как один батч (секция инбокса в контексте, кап 50) и обязан закрыть их за
|
||
ОДИН тик — сводным ответом `cli.py tg-reply <id1>,<id2>,… -c "текст"`
|
||
(одна TG-отправка, `ReplySent` на каждое входящее; полный текст хранится у
|
||
головного, у хвостовых — ссылка) или отдельными реплаями за тот же тик.
|
||
Гейт важности после сводного ответа больше не будит минуту на «хвосты».
|
||
Раньше m-516173222..228 обрабатывались 7 тиками подряд — по одному за тик.
|
||
|
||
## Инициативные реплики в чат (t-260930073419)
|
||
|
||
Харнес не только отвечает, но и сам пишет в чат: периодически — мысль/идею
|
||
по общему интересу (рост AUM кооператива) с вопросом-крючком. Механизм
|
||
(`harness/proactive.py`) без LLM: темы — детерминированный банк, ноль
|
||
кредитов роутера, предсказуемый тон.
|
||
|
||
- Событие `InitiativeSent` (aggregate `cycle`, id `proactive:<slot>`) —
|
||
идемпотентно через UNIQUE стора; проекция хранит последний слот +
|
||
историю (анти-спам стейт).
|
||
- Ритм: слот = 8-часовые коряги в окне активности 5–21 UTC (по истории
|
||
чата: пики 5–8 и 15–21, ночь — молчим); не чаще раза в 8ч; тишина 2ч
|
||
после последнего сообщения Alex (не перебиваем разговор); не пишем при
|
||
неотвеченных входящих и открытых critical-эскалациях.
|
||
- Темы round-robin: наименее использованная из банка (10 идей про AUM —
|
||
формула капитала, места 42 Labs, расходы роутера, воронка портала,
|
||
проекты, тарифы, отчёт о пользе); ответ Alex подхватывает минутный
|
||
уровень как обычное входящее.
|
||
- Запускается из delivery-демона (раз в 15с, best-effort), гейт важности
|
||
не затрагивается: LLM-тики остаются тихими.
|
||
- env-оверрайды: `SIMBIOT_PROACTIVE_INTERVAL_H`, `SIMBIOT_PROACTIVE_QUIET_H`,
|
||
`SIMBIOT_PROACTIVE_SLOT_H`.
|
||
|
||
```bash
|
||
./venv/bin/python cli.py proactive # отправить, если ритм позволяет (SKIP + план, если нет)
|
||
./venv/bin/python cli.py proactive --plan # состояние ритма без отправки
|
||
./venv/bin/python cli.py proactive --force # сейчас, игнорируя окно/тишину (демо)
|
||
```
|
||
|
||
## Без-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`.
|
||
|
||
## MCP портала в тике (t-261010093054)
|
||
|
||
Каждый LLM-тик получает инструмент `portal_mcp` — живой портал кооператива
|
||
через MCP-шлюз cil.by (JSON-RPC 2.0, агент зарегистрирован 29.09, владелец
|
||
alex). Ключевое для Alex: **звонки назначаются из чата** —
|
||
`booking_slots` (свободные слоты, call=30мин/session=20мин, график 3/3
|
||
Минск) → `booking_create` (jitsi-звонок со ссылкой, гостю письмо, запись и
|
||
транскрипция автоматом). Остальной спектр: list_calls/get_call (саммари
|
||
прошедших), list_members/projects_list/list_agents, treasury_status,
|
||
list_events, whoami/report_task/report_event.
|
||
|
||
Лимиты зашиты в код (не в промпт): **4 вызова на тик**, таймаут 15с,
|
||
обрезка результата 6k симв.; тул не поднимается без токена. Guardrail:
|
||
деньги-тулы `product_create`/`product_set_mode` (Stripe) запрещены по
|
||
умолчанию — `SIMBIOT_MCP_ALLOW_ALL=1` снимает. Ошибки шлюза возвращаются
|
||
текстом `error: …`, тик не валится; трейс — `PortalMcpCalled` в сторе.
|
||
|
||
Токен: env `SIMBIOT_PORTAL_MCP_TOKEN` → `<home>/secrets/portal-mcp-token`
|
||
(legacy-фолбэк `.simbiot-portal-token` в корне репо). Ручной вызов:
|
||
`cli.py mcp-call booking_slots '{"type": "call"}'`, список тулов —
|
||
`cli.py mcp-call --list`. Клиент: `harness/portal_mcp.py`, обёртка тика —
|
||
`_llm_worker._portal_mcp_tool_for`, тест `tests/test_portal_mcp.py`.
|
||
|
||
## Хард-кап шагов тика (e-260930031800)
|
||
|
||
Мин-воркер уходил в исследовательские цепочки (90+ tool-вызовов) и умирал
|
||
по таймауту 300с — CycleError, эскалации min→hour каждую ~50 минут.
|
||
Защита в `_llm_worker`: `BeforeToolCallEvent.cancel_tool` отменяет
|
||
вызова сверх лимита, `AfterToolsEvent.end_turn` останавливает цикл
|
||
агента ПОСЛЕ партии инструментов (без него модель шлёт шаги снова —
|
||
e2e: 20 отмен подряд), кладя в историю ассистентский текст-итог.
|
||
Нативный `limits={"turns": N}` НЕ используется: стоп на границе хода
|
||
оставляет `result.message` = toolResult → пустой дайджест (04:18+).
|
||
Лимит: `SIMBIOT_MAX_TOOL_CALLS` (по умолчанию 30, счёт по реальным
|
||
исполнениям).
|
||
|
||
## Дашборд задач
|
||
|
||
Веб-страница со всеми задачами (статусы/размеры/уровни) + лестница циклов +
|
||
эскалации + свежие дайджесты, автообновление каждые 5с (JSON-API `/api/state`).
|
||
Running-задача подсвечена бейджем «🔧 в работе: <уровень> · <elapsed>»
|
||
(t-261001082111): время взятия — `claimed_at` из проекции TaskClaimed,
|
||
таймер живёт в браузере; requeue сбрасывает `claimed_at`.
|
||
|
||
```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 открыт (токен-защищён).
|
||
|
||
## Decision-модель (Ollama /v1/systemone: облачная clef:27b → локальная nimble)
|
||
|
||
Decision-модели из бенча `symbiot/systemone-bench`. Основная — `clef:27b`
|
||
в Ollama Cloud (`https://ollama.com/v1/systemone`, ключ
|
||
`SIMBIOT_DECISION_API_KEY` в `.env` инстанса, ~0.6–0.9 с на запрос).
|
||
Запасная — локальная nimble (qwen3.5-9B Q8_0, `ollama pull nimble`,
|
||
Ollama ≥ 0.35.1, CPU 10–30 с): отвечает, если облако упало/квота/сеть.
|
||
У каждого бэкенда свой предохранитель (5 мин после сбоя). Не генерирует
|
||
текст — возвращает распределение по вариантам (`choice`) или P(да)
|
||
(`noul`); детерминирована, кредитов роутера не тратит. Схема как в
|
||
`ga/router42.py` бенча: детерминированная механика — быстрый путь,
|
||
nimble — арбитр там, где механика слепа; недоступна — тихий фолбэк.
|
||
|
||
| Потребитель | Было | С моделью | Режим по умолчанию |
|
||
|---|---|---|---|
|
||
| `tasks` — порядок батча тика | чистый FIFO | из 9 старейших первой — самая ценная для AUM, остальные FIFO | on |
|
||
| `topic` — тема инициативной реплики | round-robin | выбор из 9 наименее использованных с учётом доски | on |
|
||
| `gate` — серая зона гейта (только warn/info) | LLM-тик всегда | P(нужен LLM) < 0.35 → тихий такт | **shadow** (только пишет) |
|
||
| `agent` — `cli.py decide -q … -o k=v` | — | выбор 2–9 вариантов для LLM-агента тика | on |
|
||
|
||
Конфиг (env > `.env` инстанса): `SIMBIOT_DECISION=off|shadow|on` (дефолт
|
||
off — тесты герметичны), точечно `SIMBIOT_DECISION_<TASKS|TOPIC|GATE>`,
|
||
`SIMBIOT_DECISION_API_KEY` (есть → основной = облако clef:27b, нет →
|
||
локальная nimble без запасного), `SIMBIOT_DECISION_URL`,
|
||
`SIMBIOT_DECISION_MODEL`, `SIMBIOT_DECISION_TIMEOUT` (облако 30 с, предохранитель 60 с /
|
||
локально 90 с), `SIMBIOT_DECISION_FALLBACK=on|off`,
|
||
`SIMBIOT_DECISION_FALLBACK_URL|MODEL|TIMEOUT` (локальная nimble, 90 с), `SIMBIOT_DECISION_GATE_MIN_P` (0.35),
|
||
`SIMBIOT_DECISION_KEEP_ALIVE` (2h). Глобальный shadow — потолок для всех.
|
||
|
||
Ограничения из бенча: шортлист строго ≤ 9 (точность 100% на 6 → 50% на
|
||
10), `confidence` — не вероятность правильности. Кеш по хешу
|
||
запроса (ответы детерминированы — та же очередь между тиками бесплатна),
|
||
только min/hour/day (не sec). Каждый реальный вызов → событие
|
||
`DecisionMade` (с `backend`/`model`) (policy trace для GA-сшивки с
|
||
исходом). Статус/статистика: `cli.py decide`, `cli.py decide --stats`.
|
||
Тест: `./venv/bin/python tests/test_decision.py` (фейковый эндпоинт).
|
||
|
||
## Ядро Коди — обучаемые ученики решений (harness/kodi_core.py)
|
||
|
||
Цикл как в systemone-bench (engine42/router42): учитель → маленькая
|
||
своя модель + kNN-память новизны → роутер → дообучение на исходах.
|
||
Первая голова — `addressing` (отвечать ли в группе), режим
|
||
`SIMBIOT_CORE_ADDRESSING=shadow|on|off` (дефолт shadow: решает учитель
|
||
`clef:27b`, ученик пишет прогноз `CorePrediction`).
|
||
|
||
- **Метки** (события `core`, подписаны): учитель (живые вердикты и
|
||
разметка истории `cli.py core backfill`, вес 1), неявная обратная
|
||
связь чата (`CoreFeedback`: «не тебе/продолжает» после ответа → 0,
|
||
«@juno42bot»/«Коди?» сразу после молчания → 1, вес 3), Alex —
|
||
кнопки «Это было мне?» в личку на незнакомых спорных (≤ 5/сут, вес 5).
|
||
- **Ученик**: эмбеддинг сообщения (локальный `nomic-embed-text`, кеш в
|
||
`<home>/core/emb.sqlite`) + 8 флагов → логрегрессия (numpy); новизна —
|
||
kNN к обучающей памяти, пороги калибруются на холдауте.
|
||
- **Роутер**: знакомо и |p−0.5| ≥ 0.4 → ученик; незнакомо и учитель
|
||
сомневается → вопрос Alex; иначе учитель.
|
||
- **Тренировка**: дневной тик (и `cli.py core train`) — временной
|
||
холдаут, претендент сменяет чемпиона только при лучшем log-loss;
|
||
`CoreModelPromoted` с sha256 файла; подменённый файл не грузится;
|
||
`cli.py core rollback`. Метрики и тень за 7 дней — `cli.py core status`.
|
||
|
||
## Модели
|
||
|
||
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/портала |