diff --git a/TASK-agent-tools.md b/TASK-agent-tools.md new file mode 100644 index 0000000..85e1ba6 --- /dev/null +++ b/TASK-agent-tools.md @@ -0,0 +1,147 @@ +# Агент «Порфирий» на телефоне: скиллы + тулсеты — архитектура (договорено 2026-08-22) + +> Зафиксировано по итогам обсуждения. Это и память, и ТЗ для разработки (opencode). +> Модель сейчас: Qwen3.8-27B-NVFP4 через llm.binom.pw. План: позже перейти на +> Gemma (маленькая, быстрая, on-device) — архитектура должна это пережить без переделки. + +--- + +## 1. Ключевое разделение: СКИЛЛЫ ≠ ТУЛСЕТЫ + +- **Скиллы** = знания. «Как пользоваться», рецепты, шаги, питфоллы. + Пассивные, лежат в файлах, читаются по требованию через `read_skill`. + Отвечают на вопрос «КАК?». +- **Тулсеты** = доступные руки. «Какие функции сейчас можно дёргать». + Активные, живут в промпте, меняются динамически. Отвечают на «ЧТО МОЖНО?». +- Пересечение одно: скилл говорит «включи вот этот набор» — дальше механика делает своё. + +## 2. Ядро (всегда в промпте, 5 тул, компактно) + +1. `get_current_time` — время/дата/пояс, ISO 8601 (уже есть) +2. `show_text` — показать текст на очках (уже есть, ShowText) +3. `read_skill|имя` — вернуть полное содержимое скилла в историю +4. `enable_toolset|имя` — активировать набор тул +5. `disable_toolset|имя` — снять набор тул + +Все тулы — **идемпотентные и прощающие**: +- повторный enable — не ошибка, «уже активен, продлеваю таймаут» +- disable без активного — «нет активных наборов», не ошибка +- неизвестное имя набора — «набора X нет, доступны: media, ...» + +## 3. Структура промпта (пересобирается на КАЖДЫЙ запрос) + +``` +1. ЯДРО (всегда): 5 тул с описаниями +2. КАТАЛОГ ТУЛСЕТОВ (всегда, компактно, по строке на набор): + media — управление медиа и воспроизведением (search/info/play) + web — работа в вебе (когда появится) +3. АКТИВНЫЕ ТУЛСЕТЫ (динамически, только активные): + media: активен (+ описания тул media_search, media_info, media_play...) +4. ИНДЕКС СКИЛЛОВ (всегда, имя + триггер, по строке на скилл) +5. Содержимое скиллов — НЕ в промпте, читается через read_skill в историю +``` + +## 4. Префиксы тул — по имени набора + +Все тулы набора имеют префикс: `media_search`, `media_info`, `media_play`. +Префикс = маршрутизация: система видит `media_*` → знает набор, сигнатуры, может +проверить аргументы без реестров-в-сторону. + +## 5. Активность тулсета — на СЕССИЮ, в хиппе, по таймауту + +- Хранится в памяти (Map): `сессия → (набор → deadline)` +- **Не персистится**: перезапуск приложения = всё сброшено (это правильно: + перезапустил — значит, начал заново; 10-минутные наборы переживать рестарт не должны) +- Таймаут: **10 минут с последнего вызова тулы набора** (enable продлевает) +- Проверка при сборке промпта: `now > deadline` → набора нет, запись подчистить +- Никаких фоновых таймеров — только проверка при сборке промпта + +## 6. Три состояния промпта + +**Состояние 1 — набор не активен, тема не медиа:** +``` +ядро + каталог тулсетов + индекс скиллов +(скилл media в индексе: «для фильмов вызови enable_toolset|media») +``` + +**Состояние 2 — нейронка решила активировать (enable_toolset):** +- Вызов enable **НЕ пишется в историю** — система активирует набор в реестре, + пересобирает промпт → следующий запрос уже в состоянии 3 +- Это «невидимая системная тула»: её вызовы — часть механики, не диалога + +**Состояние 3 — набор активен:** +``` +ядро + описания тул media_* + «Активные тулсеты: media» + скилл media УБРАН +``` +- Скилл media **исчезает из индекса/промпта**, пока набор активен — иначе + нейронка видит «надо вызвать enable» и соблазняется дёрнуть его повторно. + Искушение удаляется, а не запрещается. +- Секция «Активные тулсеты» нужна: объясняет нейронке, что тулы media_* + легитимны и их можно звать (снимает паралич выбора). 5 токенов. + +**Деактивация:** нейронка вызвала disable → выкидываем вызов из истории, +снимаем набор, пересобираем промпт. Симметрично enable. + +## 7. Бесшовная подмена вызова на активацию (защита от тупки) + +Нейронка вызвала `media_search` при НЕактивном наборе (по привычке/призраку +из истории): +- Система НЕ выполняет вслепую и НЕ ругается +- Подменяет вызов: как будто нейронка вызвала `enable_toolset|media` +- Отвечает: «Тулсет media активирован. Реестр: media_search(query), media_info(id)... + Поиск ещё НЕ выполнялся. Для поиска вызови media_search с аргументом query.» +- Ключевое условие: явно сказать, что вызов ещё не выполнен — иначе нейронка + решит, что тула сработала, и выдумает ответ (галлюцинация) +- Нейронка видит чистую историю: запрос → активация → реестр → search. Никаких швов. + +Дополнительная страховка: если аргументы вызова совпадают со схемой из реестра — +можно выполнить сразу, без второго круга. + +## 8. История диалога — НЕ вычищается + +- Результаты тулов остаются в истории (это память диалога: «а что там с Ларой Крофт?») +- Призраки тулов из прошлого нейтрализуются НЕ чисткой, а: + а) бесшовной подменой (см. п.7) — модель получит реестр и повторит правильно + б) секцией активных тулсетов — «сейчас активен media, зови его тулы» +- Вычистка истории = амнезия («что ты мне показывал про Лару Крофт?» — не знаю) — НЕ ДЕЛАЕМ + +## 9. Скиллы — файловая структура + +- Папка `skills/` (на телефоне, внутренние файлы приложения) +- Формат: `name + описание-триггер в одну строку + тело (шаги, команды, питфоллы)` +- `SkillRepository` — объединяет источники (встроенные assets + пользовательские) +- `SkillRegistry` — индекс (имя + триггер), идёт в промпт +- Полное содержимое — только через `read_skill|имя` (лениво) + +Скилл media (пример содержания): +``` +Триггер: поиск фильмов, детали, запуск/управление просмотром. +Вызови enable_toolset|media, дождись ответа — в нём реестр тул: +media_search — поиск по названию (аргумент query) +media_info — детали фильма (аргумент id) +media_play — запуск/управление (аргументы...) +``` + +## 10. Требования к тулам набора (для маленькой модели) + +- Описания ЗЛЫЕ И КОРОТКИЕ: имя + одна строка + аргументы в одну строку +- Активный набор ≈ 400 токенов, не гора +- Валидация аргументов на исполнителе: невалидные → «аргументы неверны, вот схема» + +## 11. Тестирование + +- **Чистая логика — JVM-юнит-тесты** (без Android): + сборка промпта (3 состояния), подмена вызова, таймауты, идемпотентность, + каталог/реестр, префиксная маршрутизация +- **На железе** (142, adb): полный цикл — вопрос → read_skill → enable → search → ответ; + переключение сессий (активность на сессию); таймаут 10 мин (ускорить для теста); + подмена вызова при неактивном наборе; двойной enable/disable + +## 12. Известные решения (не пересматривать) + +- Протокол вызова: `TOOL_CALL: имя|аргументы` (строка, не JSON — маленькая модель) +- Активность тулсета: на сессию, в хиппе, 10 мин от последнего вызова +- enable/disable — невидимые тулы (вызовы не в истории) +- Скилл исчезает из промпта при активном наборе +- История не вычищается +- Подмена вызова на активацию — с явной пометкой «вызов ещё не выполнен»