ТЗ: агент — скиллы + тулсеты (TASK-agent-tools.md)

Архитектура (договорено 2026-08-22):
- скиллы (знания) ≠ тулсеты (руки)
- ядро 5 тул: время, show_text, read_skill, enable/disable_toolset
- префиксы тул по набору (media_*)
- активность на сессию, в хиппе, таймаут 10 мин
- enable/disable невидимы (не в истории), скилл исчезает при активном наборе
- бесшовная подмена вызова на активацию
- история не вычищается
This commit is contained in:
2026-08-22 20:15:48 +03:00
parent bbf7a020d4
commit 22369670cb
+147
View File
@@ -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 — невидимые тулы (вызовы не в истории)
- Скилл исчезает из промпта при активном наборе
- История не вычищается
- Подмена вызова на активацию — с явной пометкой «вызов ещё не выполнен»