# Задача: медиа-тулы для тулсета media (агент «Порфирий») Механика тулсетов/скиллов УЖЕ готова (AgentToolkit, Skills, Toolsets — см. TASK-agent-tools.md, закоммичено c5cbc89). Твоя задача — **реализовать сам тулсет media с исполнителем** на базе Jellyfin. Чистую механику НЕ трогать. ## Контекст (что уже есть) - `AgentToolkit` (app-phone/.../agent/AgentToolkit.kt): ядро 5 тул + диспач + подмена. Регистрация тулсета: `ToolsetRegistry.register(Toolset(...), executor)`. - `Toolset`/`ToolSignature`/`ToolsetExecutor` (Toolsets.kt): сигнатуры с префиксом `media_`, аргументы строками (`query`, `id`, `seek_ms?`). - `PhoneApp.kt` уже создаёт `AgentToolkit(skills=..., toolsets=ToolsetRegistry(), showText=...)` — тулсеты пустые, тулы не зарегистрированы. - `JellyfinClient` (lib-core/src/commonMain/kotlin/pw/binom/viewmate/core/media/JellyfinClient.kt): - `suspend fun search(userId: String, query: String): List` - `suspend fun item(userId: String, itemId: String): JellyfinItem?` - `suspend fun allItems(userId: String, types: List): List` - `fun posterUrl(itemId: String, maxWidth: Int): String` - `JellyfinItem` (JellyfinModels.kt): Id, Name, Type, Overview, ImageTags, SeriesName, SeasonId, IndexNumber, ParentIndexNumber, RunTimeTicks. - В PhoneApp: `jellyfin: JellyfinClient`, `chatDao`, `server.hub` (broadcast), `actions` (воспроизведение), `state`. ## Что сделать ### 1. Тулсет media (зарегистрировать в PhoneApp) Тулы (имена БЕЗ префикса — префикс `media_` добавляется механизмом): - `search` — поиск фильмов/сериалов по названию. Аргументы: `query` (обязательный), `limit?` (опциональный, дефолт 5). - Возврат: список строк «id: Название (Год) — Тип» (до limit), либо «ничего не найдено по запросу ...». - `info` — детали элемента по id. Аргументы: `id` (обязательный). - Возврат: Название, Тип, Год, Обзор (Overview, обрезать до ~300 символов), длительность (RunTimeTicks/10_000_000 сек), есть ли постер. - `play` — запуск воспроизведения на очках. Аргументы: `id` (обязательный), `seek_ms?` (опциональный). - Проверки: очки подключены (hub.connected > 0), видео скачано на очки (локальный путь file:////video.mkv существует на очках — через hub/каталог), звук готов на телефоне (audioReady). - Воспроизведение — через существующий механизм DetailsScreen (actions.watch(item.Id, audioIndex, localVideoPath, phoneAudioReady)) — вынеси/переиспользуй логику, не дублируй криво. - Возврат: «запущено: <Название>» или понятная ошибка с причиной («очки не подключены», «видео не скачано на очки — скачай вручную», «звук не готов»). ### 2. Юзер-ID для Jellyfin Jellyfin требует userId. Посмотри, как DetailsScreen/CatalogScreen получают userId (вероятно `app.jellyfin.users()` первый или конфиг). Используй тот же механизм, закешируй. ### 3. Скилл media (файл skills/media.md) Формат (как в Skills.kt): ``` --- name: media description: поиск фильмов, детали, запуск/управление просмотром. --- Текст скилла: объясни модели, что для работы с медиа вызвать enable_toolset|media, и опиши тулы search/info/play с аргументами. ``` ### 4. Тесты (JVM, без Android — по возможности) Механику тестировать НЕ надо (уже покрыта). Но исполнитель media_search можно покрыть, если JellyfinClient абстрагируется (интерфейс/fake). Если JellyfinClient — конкретный класс с Ktor, сделай минимальный интерфейс `MediaSource` (search/item) и обёртку — тогда executor тестируется на фейке. Не усложняй сверх меры: если это требует большого рефакторинга, опиши, что осталось непокрытым. ## Ограничения - Код только Kotlin - НЕ трогать AgentToolkit/Skills/Toolsets (механика) — только регистрация тулсета и его исполнитель - НЕ ломать существующие экраны (DetailsScreen/CatalogScreen) - Все новые файлы — в app-phone (агент живёт на телефоне) - Сборка: `./gradlew :app-phone:assembleDebug` — зелёная; `./gradlew :app-phone:testDebugUnitTest` — зелёный (все старые тесты остаются зелёными) - НЕ коммитить ## Проверка 1. `./gradlew :app-phone:testDebugUnitTest` — зелёные (старые + новые) 2. `./gradlew :app-phone:assembleDebug` — собирается 3. Опиши в ответе: какие файлы создал/изменил, как зарегистрирован тулсет, как получается userId, что осталось непокрыто тестами.