Files
view-mate/TASK-opencode.md
T
subochev 7527cf8848 LLM: интерфейс + две реализации (Qwen удалённо / Gemma 4 E2B локально) + переключатель
- LlmClient интерфейс; RemoteLlmClient (OkHttp, llm.binom.pw) и LocalLlmClient (LiteRT-LM, Gemma 4 E2B)
- LocalLlm: Gemma4E2B (SHA-256, GPU/CPU-бэкенды), buildLocalLlmPrompt (SYSTEM→systemInstruction, TOOL→user), GemmaModelDownloader (стриминг, прогресс, фолбэк источников), спекулятивное декодирование
- LlmPrefs (персистентность выбора), PhoneApp: фабрика по llmChoice, пересоздание ассистента, скачивание/отмена
- UI: GlassesInfoScreen — выбор сервер/локальная + прогресс; ChatScreen — индикатор активной модели
- Тесты: +12 (LlmLocalTest) — phone 154 зелёных
2026-08-23 02:52:22 +03:00

5.0 KiB

Задача: реализовать механику скиллов и тулсетов агента «Порфирий»

Прочитай ТЗ: TASK-agent-tools.md (в корне репо). Реализуй ТОЛЬКО механику, БЕЗ привязки к медиа-данным (медиа-тулы будут позже, отдельно).

Объём работ

1. Механика скиллов

  • Абстракция Skill: name, description (триггер, одна строка), body (текст)
  • SkillRepository: читает скиллы из папки files/skills/ (YAML frontmatter: ---\nname: ...\ndescription: ...\n---\n + markdown body)
  • SkillRegistry: индекс (имя + триггер) для промпта; readSkill(name) возвращает полное содержимое
  • Скиллов реальных пока НЕТ (механика работает на пустом каталоге или тестовых скиллах в юнит-тестах)

2. Механика тулсетов

  • Toolset: name, описание набора (для каталога), список тул (имя с префиксом набора + описание + сигнатура аргументов)
  • ToolsetRegistry: активность НА СЕССИЮ (Map сессия→(набор→deadline)), хипп, таймаут 10 минут с последнего вызова тулы набора, проверка при сборке промпта (никаких фоновых таймеров)
  • enable_toolset|имя: идемпотентно (повторный = продлить), неактивный вызов НЕ пишется в историю (системная тула-невидимка)
  • disable_toolset|имя: снять, идемпотентно («нет активных наборов» — не ошибка)
  • Неизвестное имя набора — «набора X нет, доступны: ...»

3. Сборщик промпта (три состояния)

  • Состояние 1 (набор не активен): ядро + каталог тулсетов + индекс скиллов
  • Состояние 2 (активация): enable вызывает пересборку, вызов в историю НЕ пишется
  • Состояние 3 (активен): ядро + описания тул набора + «Активные тулсеты: имя» + скилл, связанный с набором, УБРАН из промпта
  • Промпт пересобирается на каждый запрос

4. Бесшовная подмена вызова

  • Вызов тулы набора при НЕактивном наборе (например media_search): НЕ выполнять, подменить как будто вызван enable_toolset|media, ответить «Тулсет media активирован. Реестр: ... Поиск ещё НЕ выполнялся. Для поиска вызови media_search с аргументом query.»
  • Если аргументы вызова совпадают со схемой из реестра — можно выполнить сразу

5. Обработчик вызовов

  • Префиксная маршрутизация: имя тулы с префиксом набора (media_*) → набор
  • Ядро-тулы: get_current_time (есть), show_text (есть ShowText), read_skill, enable_toolset, disable_toolset
  • Все тулы идемпотентные, прощающие, с понятными ошибками
  • Валидация аргументов по реестру

6. Юнит-тесты (JVM, без Android) — ОБЯЗАТЕЛЬНО

Покрыть механику полностью:

  • сборка промпта: 3 состояния
  • подмена вызова при неактивном наборе (включая «поиск ещё не выполнен»)
  • идемпотентность enable/disable
  • таймаут (инжектируемые часы — не ждать реальные 10 минут)
  • активность на сессию (переключение сессий)
  • префиксная маршрутизация, валидация аргументов
  • read_skill: чтение из files/skills, парсинг frontmatter

Ограничения

  • Код только Kotlin (как в проекте)
  • НЕ трогать существующую логику ассистента без необходимости — встраиваться аккуратно
  • НЕ делать реальные медиа-тулы (search по каталогу и т.п.) — это отдельный этап
  • Юнит-тесты должны быть зелёными: ./gradlew :app-phone:testDebugUnitTest
  • Не коммитить без подтверждения

Проверка

  1. ./gradlew :app-phone:testDebugUnitTest — зелёные
  2. ./gradlew :app-phone:assembleDebug — собирается