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

Инструменты

Гейтинг проверяется детерминированно в реестре, до исполнения — не зависит от послушности модели. Недоступные туру инструменты в спеки провайдера не попадают вовсе (не тратят токены и не соблазняют модель). Поверх гейтов чат может работать набором инструментов — узким срезом под род задачи; всё вне набора остаётся видно модели именами и назначением и возвращается тулом tool_load. Пометки: M — только Господин, P — только разрешённые пользователи (не гости); без пометки — доступен всем участникам тура.

Видимость сведена в одну решётку (заимствовано у Codex): для каждого инструмента тур получает ровно одно из состояний — Visible (спека у модели, вызов исполняется), Reserved (строка в реестре скрытого, вызов ждёт tool_load или набор), Hidden (диспетчеризуется, но нигде не рекламируется — миграционный псевдоним переименованного инструмента, чтобы не рвать реплей старых чатов) и Forbidden с причиной словами. Из неё строятся и спеки шага, и блок Hidden tools, и отказ исполнения — три места по построению не могут разойтись, а «почему модель не видит инструмент» отвечается одним словом. Каталог GET /v1/tools показывает те же гейты полями (master_only, trusted_only, privileged_only, client_only, agent_only, lead_only, needs_agents, anchored_only, indexed_notes_only, planning_only, unlisted, on_demand).

Внутри решётки живут два разных слоя, и путать их нельзя.

Слой прав — гейты личности (master_only, trusted_only, …), белый список чата (тумблеры /settings в телеграме) и whitelist крон-джобы. Это решения Господина и щит от внедрения в чужое сообщение; из недоверенного тура их не обойти даже tool_load — базовый список остаётся его потолком. Отказ этого слоя — Forbidden.

Слой внимания — что чат видит по умолчанию, когда прав хватает на большее, чем стоит держать в промпте: глобальный tools.on_demand, активный набор и курирование сна чата. Он ничего не запрещает: скрытое им остаётся в реестре скрытого именем и назначением и возвращается одним tool_load. Отказ этого слоя — Reserved, то есть «не сейчас», а не «нельзя».

Коллизия имён — ошибка конфигурации, а не тихая тень: повторная регистрация имени пропускается (первый зарегистрированный остаётся), кричит в лог на старте и перечисляется полем collisions в GET /v1/tools. Переподключение модуля коллизией не является — оно сначала снимает весь свой неймспейс.

У каждого семейства — явная группа (задаётся при регистрации): memory, person, skill, cron, chat, web, client, ask, mcp и mcp_<сервер>; модуль приносит свою (telegram). По группе тул матчится в списках доступа и группируется в меню настроек поверхности.

Инструмент, чей выхлоп — данные, а не текст, объявляет схему результата (output_schema, JSON Schema): успешный ответ — один сериализованный объект этой формы, модель ветвится по полям, а не выискивает фразу в прозе. Wire-поля для этого у провайдеров нет — схема дописывается хвостом описания инструмента. Первый такой — agent_list; для большинства инструментов текст лучше, и это осознанно не обязательно.

Ошибка инструмента — двух классов (заимствовано у Codex). Обычная уезжает модели текстом в tool_result с is_error, и тур продолжается — модель чинится сама, на этом держится самоисправляющийся цикл. Фатальная (ядро гасится или неконсистентно) рвёт тур: пары tool_use/tool_result закрываются синтетикой, неисполненные соседние вызовы не исполняются, клиент получает ошибку тура — вместо тридцати повторов вызова в сломанное.

Встроенные

Памятьmemory_save (M, с дедупом: похожая запись подкрепляется, а не дублируется; ответ показывает похожие, чтобы противоречие разрешалось на месте), memory_search (гибридный поиск, подкрепляет найденное; личное чужому туру не отдаёт), memory_list (M; дамп с фильтрами kind/since_days и страницами), memory_update (M; правит содержимое, вид, важность и приватность), memory_forget (M).

Навыкиskill (загрузить тело по имени; с моделью-исполнителем — изолированный оффлоад), skill_create / skill_edit / skill_delete (M). У навыка есть видимость: private (по умолчанию) живёт только там, где работает сам Господин, — в публичном чате такой навык не перечисляется в промпте и не исполняется; surface виден везде.

Наборыtoolset (переключить активный набор; scope chat или turn, имя null снимает), tool_load (вернуть скрытый инструмент или семейство на текущий тур), tool_find (найти инструмент по тому, что он должен делать, когда имени не знаешь), toolset_create / toolset_edit / toolset_delete (M). toolset, tool_load, tool_find и skill видны при любом наборе — иначе из узкого набора не выбраться. Подробности — Наборы инструментов.

Кронcron_create (M), cron_list, cron_log (журнал исполнений), cron_update (M, правка на месте), cron_delete (M), cron_fire (M, выстрелить сейчас: задача уходит в ближайший тик планировщика, следующий регулярный запуск — через интервал от этого). Джоба живёт в собственном чате на полке cron — его заводит ядро при создании и уносит вместе с джобой; поселить таймер в чужую переписку нельзя ни тулом, ни POST /v1/cron: оттуда он пошёл бы по общей истории с людьми и заговорил вслух в их группе.

У create/update есть model — модель туров джобы (пишется в модель её чата), — compaction_profile (low/med/high, в cron_update ещё и auto: тот же профиль компакции, что у chat_compact_profile, только прибивается чату джобы; без него автоматика даёт крон-турам low) — и tools: whitelist имён и групп, всё вне списка джобе недоступно. Узкий список заметно облегчает её промпт — спеки лишних тулов не уезжают провайдеру на каждое срабатывание; пустой массив в cron_update снимает ограничение. Whitelist сверяется с реестром на входе: незнакомое имя — ошибка с ближайшими похожими и списком групп, а не молчаливо немая джоба, которой нечем отправить результат.

Деньги видны на обоих уровнях: cron_log показывает цену каждого срабатывания, cron_list — сколько джоба потратила за всю жизнь и сколько раз стреляла. Цена запуска считается по spend.run_id, а не окном по времени: в общем чате окно поймало бы и параллельный живой тур. Итог живёт на самой джобе — журнал подрезается пятьюстами записями, а правка полей идёт UPDATE-ом по id и счётчик не трогает.

Командаagent_spawn (M, подагент на своей модели и со своим набором; report_schema делает финальный доклад типизированным), agent_send (M), agent_wait (M), agent_inbox (M), agent_list (M, выхлоп — JSON по объявленной схеме), agent_stop (M); изнутри тура подагента — agent_say и agent_done. Пятеро follow-up’ов ждут живой команды (needs_agents): без подагента их нет ни в списке, ни в реестре скрытого, а agent_spawn открывает семейство сам, ещё в том же ходу. Подробности — Агентские команды.

Личностиperson_list, person_merge (M), person_rename (M).

История и контекст (все M) — history_search (поиск по всем чатам ядра и сырому телеграм-архиву: подстрока или регулярка; по умолчанию ищет только речь, служебные вставки ядра — с флагом include_service), chat_clear (стереть переписку чата начисто), chat_compact (свернуть историю не дожидаясь порога), chat_rebuild (пересобрать саммари с нуля окнами merge+squeeze, с живым прогрессом), chat_summary_get / chat_summary_set, chat_compact_profile (прибить чату профиль компакции — low, med или high, пороги крона, телеграма и лички; auto возвращает автоматику), chat_sleep_start (курировать набор этого чата прямо сейчас, не дожидаясь его ночной границы — тоже с живым прогрессом), usage_stats (свой расход, доля мыслей в нём и точность триажа).

Ходlift_turn_limit (M): снять потолок итераций до конца текущего тура, когда задача честно требует много шагов. Разрешение спрашивается заранее и явно (ask_questions), инструмент только применяет решение. Тому же служат поле uncapped_iterations в запросе тура (клиент решает за Господина) и uncapped у набора инструментов (решает род работы) — этот путь для случая «поняли посреди тура».

think_harder (M) — из того же ряда, но про глубину, а не про длину: поднять уровень рассуждения на оставшиеся итерации хода, когда задача оказалась глубже, чем выглядела. Каждый следующий шаг после этого стоит дороже, поэтому зовётся он один раз и осознанно; понизить уровень до конца хода нельзя. Уровень тура и без него берётся из конфига (llm.reasoning, effort записи модели), настройки чата, поля effort в запросе или слова ultrathink в реплике Господина.

Блокнот (все M) — chat_note_save (записать или переписать заметку целиком), chat_note_forget, chat_note_read (достать тела пачкой; виден, только когда блокнот показан индексом), chat_note_compact (пересобрать блокнот, не дожидаясь порога). Семейство тихое: в лентах-мессенджерах эти шаги не рисуются (quiet).

План работы (M, тоже quiet) — turn_plan: одна операция на вызов (init / start / done / drop / append / view), ответ — состояние плана целиком. Виден не всегда: только рабочему туру (клиентские операции, набор с narrate, подагент) или тому, где план уже открыт.

Правила поверх генерации (все M) — stream_rule_list, stream_rule_create, stream_rule_edit, stream_rule_delete. Правила — данные: что матчится (имя вызова, regex по аргументам), в каком окружении правило живёт и что оно тогда скажет. Список висит не в промпте, а показывается по требованию.

Внимание и одобрения (все M) — chat_attention (пометить чат «посмотри сюда», опционально с уведомлением), chat_delete (мягкое удаление: чат уходит из списков, история цела и восстановима), approval_request (спросить разрешение асинхронно — вопрос ждёт в очереди Господина, тур не блокируется), approval_status (проверить свой запрос или всю очередь), notify_master (постучаться по настроенным путям).

Вебweb_search (SearXNG; есть только при заданном web.searxng), web_fetch, wikipedia, fandom, psychonautwiki, booru_search (danbooru/e621/gelbooru, учётки из web.booru снимают анонимные лимиты). Вики-инструменты отдают и картинки: главную — всегда, а images: N — галерею статьи (служебные логотипы и иконки отсеиваются). Ходить за ними в MediaWiki API через web_fetch не нужно.

Shellshell (M): sh -c на машине ядра или на одном удалённом хосте по ssh (shell.remote); таймаут 60 с (настраивается на вызов). command берёт и массив: команды идут по порядку, каждая отвечает своим блоком == $ cmd, пачка встаёт на первом провале (сделанное — блоками, непопробованное — списком); таймаут — на команду. Это ответ модельной привычке клеить echo ==x== между командами; с yield_ms пачка не сочетается — сессия одна. Вывод по умолчанию не режется — опциональный потолок задаётся shell.output_cap; удерживается до 1 МиБ на поток головой и хвостом, выброшенная середина называется счётом ([middle cut: N chars of M]). Вывод виден по ходу, а не после: снимки хвоста уезжают событием tool_progress (в ленте элементов — item_delta по элементу вызова), с троттлингом, потолком числа событий и затиранием секретов у истока; в историю живой хвост не пишется. Долгие и интерактивные команды не убиваются таймаутом, а живут сессией: yield_ms у shell возвращает {session_id, output, chunk_id, wall_time_seconds} вместо блокировки, дальше shell_write (M) дописывает ввод и/или забирает свежий вывод (chunk_id растёт с каждой выдачей — повтор отличим от нового; пустой опрос ждёт не меньше 5 с). Уложившаяся в выдержку команда отвечает тем же JSON с exit_code и без session_id. Сессий не больше 16, бездействие дольше 5 минут убивает процесс вместе с группой; отмена тура (Esc) прибивает сессии, рождённые этим туром. Отдельных подтверждений у сессий нет — они наследуют гейт shell: личность, не канал (из телеграма Господин тоже может).

Кодингcode_task (есть только при заданном llm.code_model): одиночный оффлоад-запрос модели посильнее. Инструментов, файлов и истории у неё нет — она видит ровно то, что вписано в task, и отвечает текстом. Поэтому он для самодостаточных вопросов (алгоритм, разбор вставленного куска), а работу внутри репозитория забирает agent_spawn с кодовым набором: та же модель, но с руками. Тем, кто путает эти два пути, помогает tools.on_demandcode_task уходит из списка по умолчанию и достаётся по tool_load.

Рисованиеimage_generate (M; есть только при рисующей модели — llm.image_model либо первая из models с image_output): одиночный оффлоад-запрос рисующей модели — генерация по prompt и правка существующей (source: id сохранённой картинки, last — последняя картинка чата, нарисованная или присланная фотографией, путь к файлу — машина Господина при клиенте на связи, иначе хост ядра, — либо http(s)-URL; формат файла проверяется магик-байтами). Картинка ложится в блоб-стор, модели возвращается image <id> saved: <url> — дальше телеграм берёт её по id (telegram_attach_photos), GUI — по URL. Расход пишется на чат. Если модель тура сама умеет рисовать (image_output у неё), инструмент из её списка спрятан: она рисует прямо в ответе, без второго платного вызова. Правило рисования в промпте идёт за досягаемостью инструмента: не пускает белый список чата — правила нет, и обещания тоже.

Клиентские (client_ops, исполняет клиент — eva-tui) — read_file, write_file, write_diff, apply_patch, find_pattern, local_shell: событие client_op в живой стрим, клиент постит результат в /v1/chats/{id}/ops/result. Операцию находит по одному op_id, поэтому отвечать на неё можно через ручку любого чата — на этом стоит работа подагентов с файлами. Действий на проводе три — read, write, shell: write_diff и apply_patch ядро собирает из чтения и записи, реализовывать их клиенту нечего. Кнопки подтверждения задаёт поле decisions события, а любой исход ожидания — ответ, таймаут, конец или отмена тура — снимается событием request_resolved: диалог у клиента закрывается по факту, а не по своему таймеру (контракт — в API). Тем же проводом, но БЕЗ участия модели, ядро на входе в тур собирает файлы проекта в блок [project] (служебное действие list_up — см. API): все AGENTS.md от корня проекта до рабочего каталога и память проекта .eva/MEMORY.md, с общим бюджетом и происхождением каждого куска. Правила сборки одни на всех клиентов и живут в ядре (context.project); клиент объявляет поддержку флагом project_docs в send_message и перестаёт возить AGENTS.md в context сам.

Тулы тура (extra_tools в send_message) — клиент объявляет модели свои тулы на один тур поверх реестра; исполняет их сам, тем же проводом ops/result, что и клиентские операции: вызов приходит событием extra_tool_call, ядро ждёт (таймаут из объявления, до 900 с), ответ — блоками. ends_turn в объявлении делает тул вердиктом: успешный вызов закрывает тур. Так телеграмный мост объявляет telegram_skip — вердикт «молчу», который существует только в телеграмных турах и не мозолит глаза модели в остальных. Контракт и типизированный SDK — в API.

read_file отдаёт окно строк с шапкой [lines A-B of N]. Окно длиннее одного ответа обрывается по границе строки, а не вырезается серединой, и шапка называет offset, с которого читать дальше: дочитывание точное, а не «перечитать сначала и надеяться». Тот же приём у списка находок find_pattern. Такой выхлоп объявляет себя оконным (windowed_result), и гигиена контекста режет ему ХВОСТ по своему, более щедрому потолку (context.reader_result_max_chars): общий рез серединой верен для вердикта сборки и губителен для файла — он выбрасывает ровно то, за чем звали, а шапка продолжает обещать диапазон, которого в промпте уже нет. Хвостовой рез подписывается и зовёт сузить окно.

Файловые тулы берут пачку: path у read_file, read_hex и find_hex принимает массив путей (секции == путь, отказ одного файла — строка его секции, а не общий провал), write_file пишет несколько файлов полем files, write_hex кладёт несколько заплат полем writes — по порядку, с остановкой на первом отказе и честным списком того, что уже легло и что не начиналось. Иначе модель тянется читать пачку через cat в шелле.

Пачка правок (там же) — apply_patch: формат Codex (*** Begin Patch / Update|Add|Delete File / Move to / *** End Patch), несколько правок и несколько файлов одним вызовом — сосед write_diff, а не замена: для одиночной замены тот проще. Ни одного номера строки — место находится поиском процитированных строк, строгость ослабляется в четыре шага, вплоть до нормализации типографики: ASCII-патч ложится на файл с «ёлочками» и длинными тире. Двухфазно: сперва весь патч читается и проверяется против диска, и только потом пишется — неверный ханк не оставляет файлы наполовину переписанными, а ошибка цитирует вход, и модель чинится со второй попытки. Delete File/Move to исполняются только на рабочей машине: клиентский провод знает лишь read/write, на машине Господина это local_shell (rm/mv).

Поиск по файлу (там же) — find_pattern: регулярное выражение и строки, в которые оно попало, с их номерами. Поиск построчный, как у grep, — ^ и $ держатся краёв строки, . через перевод строки не ходит; флаги буквами (i, x, U). Файл течёт мимо потоком и не ложится в память, а набрав max_hits, чтение гасится на месте: в логе на сотни мегабайт попадание в начале стоит миллисекунды. Начало пропускает сама машина (from_line), так что листать находки дёшево. Длинная строка показывается окном вокруг вхождения — в минифицированном коде обрезка с начала спрятала бы само совпадение. Двоичный файл — отказ и отправка в find_hex.

Двоичные (там же) — read_hex, write_hex, find_hex: read_file от бинаря отказывается и отправляет сюда. Клиент возит сырые байты в base64, а дамп рисует и хекс-строку разбирает ядро — формат один на все клиенты и под тестами. read_hex отдаёт классический дамп со смещением, шестнадцатеричными байтами и колонкой расшифровки; шапка [bytes A-B of N] говорит, где ты и сколько всего. Колонка по умолчанию ASCII, но для японских игр её задают cp932 или utf-16le — тогда строки в архиве читаются. write_hex пишет байты по смещению поверх существующих (truncate обрубает хвост, смещение 0 плюс truncate — полная замена), find_hex ищет образец хексом или текстом в заданной кодировке и отдаёт смещения. Потолок дампа — 8 КБ на вызов: дамп раздувает данные всемеро, и это уже около 12 тысяч токенов.

Вопросы кнопкамиask_questions (P): 1..3 вопроса с готовыми вариантами одним вызовом, тур ждёт ответа внутри тул-колла. Кнопки рисует поверхность из tool_call-события (телеграм — inline-кнопками, tui и android — своими), ответ приходит в POST /v1/chats/{id}/questions/answer. Вопрос адресуется полем question — позицией в массиве questions вызова; номер стабилен, ответы на соседние вопросы его не сдвигают. Одновременные вызовы в одном чате развязывает call_id (id из tool_call-события); без него ответ идёт в самый ранний живой вызов. Сколько ждать — tools.ask_ttl; по истечении вопросы растворяются, и тур продолжается без ответа.

Телеграмные (telegram_send_to_other_chat, telegram_react, telegram_skip и прочие) — не встроенные: их приносит модуль поверхности, и живут они в группе telegram наравне с остальными.

Внешние (TOML + stdin/stdout)

Файл tools.d/weather.toml:

name = "weather"
description = "Get current weather for a city"
command = ["/usr/local/bin/weather-cli"]
timeout = "30s"

[input_schema]
type = "object"
[input_schema.properties.city]
type = "string"

Вход инструмента приходит JSON’ом в stdin, stdout становится результатом, EVA_CHAT_ID в окружении говорит, из какого чата позвали. Ненулевой код выхода или таймаут — ошибка инструмента, модель получит её текстом.

MCP

Секция mcp.servers: stdio (ядро запускает и держит процесс) или streamable HTTP (url + заголовки). Инструменты сервера появляются в реестре как mcp_<сервер>_<инструмент> с группами mcp и mcp_<сервер>. Имя приводится к алфавиту провайдеров и потолку в 128 символов; слишком длинное не режется молча — хвостом ставится стабильный отпечаток полного имени, так что два длинных имени с общим началом не схлопываются, а имя не меняется между перезапусками (нестабильное имя рвало бы префикс промпт-кэша).

Схема каждого инструмента чистится от шума ($schema, title, examples…) и, если после этого занимает больше 5000 байт JSON, ужимается с потерями: описания до первой фразы → словарь $defs за борт → вложенное глубже третьего уровня в {} → ветвления anyOf/oneOf/allOf в {} — остановка, как только влезло. Имена аргументов верхнего уровня выживают всегда. Так один сервер с сорокакилобайтной схемой не съедает бюджет описаний целого тура; свои инструменты сжатие обходит. Представиться сервер может сам — _meta["dev.eva/about"] в ответе initialize. Этой фразой его семейство показывается модели, когда активный набор свернул его в одну строку. Поле description в конфиге сервера — ручной override поверх сказанного сервером и единственный способ описать тех, кто _meta не умеет (FastMCP не умеет). Промолчали оба — в строке будут первые имена тулов.

Сервер может украсить свои тулы через _meta (конвенция dev.eva/*): _meta["dev.eva/icon"] — эмодзи-иконка, _meta["dev.eva/brief"] — шаблон краткого показа вызова по полям входа («{from} → {to}» рисуется как «Череповец → Москва»). Оба уезжают клиентам в каталоге GET /v1/tools (поля icon/brief, у встроенных тулов иконки свои) — их рисует любая поверхность, показывающая ход тура; провайдеру LLM не отправляются. Чужие клиенты незнакомые _meta-ключи обязаны игнорировать — конвенция ничего не ломает.

Там же _meta["dev.eva/anchored"]: тул работает только в туре с якорем, то есть отвечающем на конкретное сообщение поверхности. В туре без якоря (кодовая сессия, крон) такой тул в спеки не попадает — его вызов всё равно кончился бы ошибкой, а спека тратила бы токены и подсовывала модели тупиковый выбор. Объявляет это модуль о себе сам, и ядру не приходится знать, что у него за поверхность. Рядом _meta["dev.eva/master"] — тул только для туров хозяина: так поверхность запирает своё администрирование, не запирая разговор.

И _meta["dev.eva/ends_turn"]: успешный вызов закрывает тур — ядро не возвращает управление модели, а сразу уходит в завершение. Для инструментов-вердиктов вроде telegram_skip: «молчу» уже сказано, следующая итерация могла бы только передумать или зациклиться на повторных skip. Ошибка вызова тур не закрывает — несостоявшийся вердикт вердиктом не является.

Последний гейт конвенции — _meta["dev.eva/unlisted"]: миграционный псевдоним. Тул зовётся по имени, но не рекламируется ни спекой, ни реестром скрытого — так модуль переименовывает инструмент, не ломая реплей чатов, где модель помнит старое имя.

Доступ гейтится: access: master | allowed | everyone и trusted_only — только из доверенного окружения. У MCP-сервера дефолт мастерский: чужой бинарь может уметь что угодно, открывать его чужим — осознанное решение. У модуля дефолт everyone: его ставит сам оператор, и обычно это поверхность, чьи инструменты и есть ответ Евы в чужом туре, а что внутри администрирование — модуль запирает поштучно. MCP-сервер — горячезаменяемый: stdio-подключение ленивое, упавший процесс перезапускается следующим вызовом с новым handshake, а протухшее соединение долгоживущего модуля переподключается с одним автоповтором — рестарт модуля невидим для модели. tools/list кэшируется в базе: лежащий на старте ядра сервер поднимается с тулами из кэша и оживает первым же вызовом; совсем без кэша — выключается целиком, ядро живёт дальше. Каталог готовых серверов — MCP-серверы.

Модули

Модуль — долгоживущий соседний процесс с тем же MCP-протоколом (line-JSON-RPC) по UDS-сокету или TCP; живёт своим systemd-юнитом, hot reload — рестарт юнита. Регистрируется на лету, без рестарта ядра тулами (все M): module_list (зарегистрированные, их статус и кандидаты из modules.socket_dir), module_register (socket или tcp — тулы появляются сразу; так Ева сама пишет себе модуль и включает его), module_approve (подключить кандидата из socket_dir), module_unregister (выгрузить). Решения живут в базе и переживают рестарты; ядро само замечает уход/возврат сокета (выгрузка/переподключение со свежим tools/list) и никогда не подключает ничего без явной команды. Статичные модули можно объявить конфигом (modules.servers) — те управляются конфигом и рестартом, как обычные MCP.