Архитектура (договорено 2026-08-22): - скиллы (знания) ≠ тулсеты (руки) - ядро 5 тул: время, show_text, read_skill, enable/disable_toolset - префиксы тул по набору (media_*) - активность на сессию, в хиппе, таймаут 10 мин - enable/disable невидимы (не в истории), скилл исчезает при активном наборе - бесшовная подмена вызова на активацию - история не вычищается
11 KiB
Агент «Порфирий» на телефоне: скиллы + тулсеты — архитектура (договорено 2026-08-22)
Зафиксировано по итогам обсуждения. Это и память, и ТЗ для разработки (opencode). Модель сейчас: Qwen3.8-27B-NVFP4 через llm.binom.pw. План: позже перейти на Gemma (маленькая, быстрая, on-device) — архитектура должна это пережить без переделки.
1. Ключевое разделение: СКИЛЛЫ ≠ ТУЛСЕТЫ
- Скиллы = знания. «Как пользоваться», рецепты, шаги, питфоллы.
Пассивные, лежат в файлах, читаются по требованию через
read_skill. Отвечают на вопрос «КАК?». - Тулсеты = доступные руки. «Какие функции сейчас можно дёргать». Активные, живут в промпте, меняются динамически. Отвечают на «ЧТО МОЖНО?».
- Пересечение одно: скилл говорит «включи вот этот набор» — дальше механика делает своё.
2. Ядро (всегда в промпте, 5 тул, компактно)
get_current_time— время/дата/пояс, ISO 8601 (уже есть)show_text— показать текст на очках (уже есть, ShowText)read_skill|имя— вернуть полное содержимое скилла в историюenable_toolset|имя— активировать набор тул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 — невидимые тулы (вызовы не в истории)
- Скилл исчезает из промпта при активном наборе
- История не вычищается
- Подмена вызова на активацию — с явной пометкой «вызов ещё не выполнен»