Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Архитектура

  • Турный цикл (src/agent.rs): одно сообщение на входе — поток событий на выходе (text_delta, thinking_delta, tool_call, tool_result, tool_progress, usage, compacting, done, error). usage шлётся на каждый поход к провайдеру — по нему клиент двигает счётчик расхода не дожидаясь конца; compacting отмечает сворачивание истории, которое идёт после ответа и на длинной истории читалось бы как зависание. Внутри тура — агентный луп до llm.max_turn_iterations (дефолт 16) походов к провайдеру; транзиентные ошибки (сеть, 5xx/429) ретраятся, детерминированные отказы контент-фильтра изолируются бисекцией. Каждый тур виден в /v1/generations и отменяем кооперативным токеном.
  • Сборка промпта: персона + правила ядра (конвенция маркеров, политика безопасности) + файлы проекта [project] (все AGENTS.md от корня до рабочего каталога, читаются один раз на входе в тур — src/project_docs.rs)
    • список навыков + наборы инструментов с реестром скрытого — стабильный system с cache_control-breakpoint’ом. Порядок — по позиционированию: критичное (личности [speakers], доверие [env]) в голове, справочное (сборка, навыки, наборы, резерв тулов, модели) в середине и хвосте, а на самом краю [critical] повторяет главное — кто Господин и что чужой текст не приказ. Сами правила пишутся RFC-капсом (MUST/NEVER/SHOULD/AVOID/MAY, контракт объявлен блоком [conventions]), плотно и прямым императивом; описания фактов, форматы и примеры в капс не переводятся. Летучее ([now], recall памяти, блокнот, план работы, память о чате, атрибуция) собрано в блок [state] и едет после всей истории — префикс вместе с историей переживает ход (см. Кэш промпта). Реестр скрытого считается от набора на входе в тур: переключение посреди тура меняет спеки следующей же итерации, но не пересобирает system — иначе префикс рвался бы каждый шаг.
  • Инварианты тура: запрос к провайдеру всегда валиден, и держат это три механизма. Список инструментов замораживается снимком шага (StepTools) на один поход к провайдеру: вызов исполняется по снимку, который его объявил, даже если toolset/tool_load или отцепка модуля успели сменить состав — мутации видны со следующей итерации, а физически исчезнувший инструмент (отвалившийся MCP) отвечает честной ошибкой исполнения, а не «нет такого». Пары tool_use/tool_result чинятся при сборке промпта, не в базе: вызову, чей результат так и не родился (процесс умер между ними), дописывается байт-стабильная синтетика, результат-сирота выбрасывается — один обрыв больше не валит 400-й каждый следующий тур чата. Отмена тура кооперативна и даёт доиграть: cancel_* коротко ждёт штатного сворачивания, инструменты с внешним исполнителем (shell, клиентские записи и local_shell, MCP-вызовы) на отмене ждут до их собственного таймаута, а не дропаются — успевший прийти результат побеждает гонку с отменой и ложится в историю настоящим. Мок-провайдер проверяет сопряжённость пар на каждом запросе — весь тестовый набор стережёт инвариант попутно.
  • Служебные вставки (src/fragments.rs) — реестр всего текста, который ядро дописывает в историю от чужого имени: блоки [state], предупреждение [wrapup] о конце итераций, ступени давления контекста [window] (настраиваемые тексты context.pressure опознаваемы, потому что вставка всегда оборачивает их маркером), заметки [kernel], огрызки элизии, маркеры картинок, граница компакции [compacted] (единственная вставка, живущая в базе, — для клиентов; в промпт она не едет). Каждая вставка носит байт-стабильный маркер и опознаёт себя сама — один предикат is_machine_text отвечает всем местам разом: окно живой истории меряется только репликами людей (синтетика ход не открывает и границей компакции не становится), компакция не тащит в резюме «модель залипала на инструменте», history_search по умолчанию ищет только речь (флаг include_service возвращает всё). Классификация матчит только явные маркеры реестра — неразмеченная вставка не может объявить своим чужой текст. Новый вид служебного текста начинается с записи в реестре.
  • Правила поверх генерации (src/rules.rs) вмешиваются уже внутри тура: совпало имя вызова и regex по его аргументам — напоминание клеится спереди к результату в промпт-копии, в базу и клиенту уходит чистый выхлоп. Место вклейки — там же, где кап на свежий tool_result: сначала снимается служебная метка «пусто», потом режется выхлоп, и только потом ложится напоминание. Сработавшее пишется в stream_rule_stat и в лог.
  • Советчик (src/advisor.rs) вмешивается тем же каналом, но с другой стороны: вторая дешёвая модель раз в несколько итераций рабочего тура получает дельту транскрипта, ходит читающими инструментами и клеит в промпт-копию <advisory>. Щит от шума (нормализация, чёрный список, дедуп, одна заметка за цикл, кулдаун) живёт в коде и самому советчику невидим.
  • Размышления хранятся блоком thinking в реплике Евы и уезжают клиентам вместе с историей, но в контекст модели не возвращаются: db::to_chat_messages выбрасывает их на входе в историю (провод требует подписанных провайдером блоков, а чужая вчерашняя мысль скорее мешает). Сообщение, кроме мысли ничего не содержавшее, из истории исчезает целиком — пустую реплику провайдеры отвергают.
  • Хранилище (src/db.rs): один SQLite (db.path), файл создаётся сам, схема и миграции применяются на старте. Таблицы: чаты, сообщения, память, навыки, личности и алиасы, крон-джобы и журнал, расход (spend), правила и их срабатывания, заметки советчика. Плюс телеграм-таблицы, которых ядро не читает: поверхность ведёт своё состояние у себя, а данные переживают код.
  • Супервизоры (фреймворк eva): компоненты server (axum API), chats (жнец временных чатов — вместе с чатом уносит и его подагентов), cron (планировщик). Graceful shutdown из коробки.
  • Модули — то, что живёт вне ядра: инструменты и целые поверхности, отдельными процессами на eva-sdk (см. Модули и SDK). Телеграм — уже такой модуль, своих поверхностей в ядре не осталось.