--- title: Session Design — AI Psychologist App aliases: - Дизайн сессии - Session design - архитектура сессий created: '2026-05-19' updated: '2026-06-02' tags: - project - app - psychology - ux - architecture related: - '[[personal/projects/psychologist-app/overview]]' - '[[personal/projects/psychologist-app/character-design]]' - '[[personal/projects/psychologist-app/onboarding-ux]]' --- # Session Design — AI Psychologist App > Детальный дизайн сессии: UX-флоу, механика вопросов, открытие/закрытие. > Связан с: [[overview]] (раздел 5-6 — базовая структура и архитектура ботов) --- ## 1. Механика вопросов (Question Delivery) ### Pipeline вопросов ``` Бот 2 (Analyst / "Психолог") → генерирует 1-2 вопроса в 3-м лице ("пользователь избегает...") ↓ Бот 1 (Narrator / "Посредник") → переформулирует в 1-е лицо ("ты избегаешь?") → на выходе: структурированный массив вопросов для UI ↓ UI алгоритм (не агент!) → показывает вопросы по одному в карточках → ждёт ответа → следующий ``` **Ключевой принцип**: раздача вопросов — детерминированный алгоритм, не LLM-вызов. Агент генерирует батч вопросов один раз → посредник переформулирует → UI сам управляет порядком. **Narrator детектирует 3-е лицо**: через prompt rule ("переформулируй в 1-е лицо") — регекс не нужен. ### Завершение батча вопросов → подбивка Когда все вопросы в батче отвечены, UI передаёт агенту нумерованную подбивку ответов. Это входные данные для следующей итерации (следующий батч или инсайт). --- ## 2. Открытие сессии ### Структура intro 1. **Персонаж-активация** (анимация, 1-2 сек) 2. **Короткое введение** — 1-2 предложения, сразу к делу, без "Давай начнём с..." 3. **"Крючки-активаторы"** — max 2 из profile.md / последних сессий. Если нет истории — пропустить. 4. **Переход к теме** — предложение агента или выбор пользователя ### Что получает Analyst при старте - `profile.md` — паттерны пользователя - последние 2 сессии из `observations/` - задача: intro + max 2 крючка --- ## 3. Закрытие сессии ### Последовательность (3 этапа) #### Этап 1 — Structured session close (backend, не видно пользователю) Analyst генерирует `SessionCloseResult`: - `insight` — ключевое наблюдение (1-2 предложения, 3-е лицо) - `sessionSummary` — аналитическое резюме всей сессии (не первые реплики) - `profileUpdates` — новые паттерны для добавления в `profile.md` - `promptUpdates` — обновления для `prompts.md` (если применимо) Приложение раскладывает результат по файлам через `ProfileManager`. > **Статус**: частично реализован. `insight` генерируется (Sonnet + tools + полный диалог). > `sessionSummary`, `profileUpdates`, `promptUpdates` — НЕ реализованы (см. §12.2.G). #### Этап 2 — Посредник: человечный итог (видно пользователю) - Тёплая живая фраза, без психолого-языка - Не повторять аналитика, не хвалить избыточно - Упомянуть дату следующей сессии если есть расписание #### Этап 3 — "One last thing" + завершение - Открытый вопрос: "Хочешь что-то добавить?" - Маленький текстовый input, неакцентная кнопка "Завершить" - Если написал → агент добавляет в заметки сессии, отвечает коротко (не перезапускает сессию) - Если упомянул перенос → сценарий планировщика (EventKit) --- ## 4. Флоу сессии целиком ``` ОТКРЫТИЕ ├── Анимация персонажа (1-2 сек) ├── Intro: 1-2 предложения + max 2 крючка из истории └── Выбор темы ДИАЛОГ ├── Analyst генерирует 1-2 вопроса (3-е лицо) ├── Narrator переформулирует → карточки (1-е лицо) ├── UI раздаёт по одному (one-card UX) ├── Пользователь отвечает → нумерованная подбивка └── Следующий батч / инсайт └── session_complete: true → карточка "Завершить / Продолжить" └── hard limit: 8 ходов → принудительный readyToClose ЗАКРЫТИЕ ├── [backend] Analyst: insight + sessionSummary + profileUpdates ├── Посредник: тёплая фраза + дата следующей сессии ├── "One last thing" │ ├── Написал что-то → в заметки, короткий ответ │ ├── Упомянул перенос → сценарий планировщика │ └── Завершить (кнопка) → сессия закрыта ``` --- ## 5. State Persistence & Session Recovery ### Текущая реализация (актуально) `resumeOrStart()` при открытии SessionView: - Проверяет незавершённую сессию (`detectIncompleteSession()`) - `.resumed` → восстанавливает карточки из `session.messages` (видимые `.analyst`) - `.startedAfterPartialSummary` → была стale сессия >20h → показывает partial summary, затем чистый старт - `.started` → обычный старт `IncompleteSessionState.isFresh` — сессия считается свежей если < 20 часов. Stale → `generatePartialSummary()` → `completed_partial`. ### Кнопка "Начать заново" при resume При восстановлении сессии (`.resumed`) показывать кнопку **"Начать заново"** рядом с первой карточкой. - Кнопка исчезает после первого отправленного ответа пользователя - Нажатие → `cancelSession()` + `startNewSession()` (чистый старт) - Реализовать как `SessionViewModel.showRestartButton: Bool`, сбрасывается в `send(text:)` ### Что не реализовано (Stage 2) Retention protocol: при N пропущенных сессиях подряд → реактивационный флоу с push-уведомлением. --- ## 6. Что реализовано сейчас > Актуально на 2026-06-02, коммит `53c1d2cb`. ### Пейсинг сессии - `AnalystJSON.sessionComplete: Bool` (backward-compatible) - `ProcessingResult.readyToClose([String]?, observation:)` - `SessionManager.userTurnCount`, `consecutiveShortAnswerCount` - Hard limit: 8 ходов → принудительный `.readyToClose` - `continueSession()` — сброс флага после "Продолжить" - Пейсинг-правила и директивы усталости в systemPrompt AnalystBot ### Session Recovery - `detectIncompleteSession()`, `resumeOrStart()`, `generatePartialSummary()` - `FatigueContext`, `IncompleteSessionState`, `SessionResumeResult` - SessionViewModel: восстановление карточек при `.resumed` ### Завершение сессии - `endSession()` → `generateSessionInsight()` (Sonnet + tools + полный диалог, без обрезки) - Агент вызывает `search_sessions`, `read_session`, `read_profile` для сравнения с историей - `finalInsight` → `ProfileManager.addInsight()` немедленно - `suggestedInsight` по ходу диалога → `addInsight()` немедленно (не теряется при X) ### Агент / промпты - NarratorBot: legit-маркер, injection guard, off-topic директива, детекция усталости/настроения - AnalystBot: turnCount/duration/messageCount/fatigueCtx в user message; no-filepath директива ### UX - One-card UX: `CardData.isActive`, `appendCard()`, `activeCard`, `historyCards` - История — dimmed compact cards (read-only) - `TextEditor` с placeholder, auto-grow (36→160pt), кнопка dismiss клавиатуры - `CardData.isCompletion` → quickReply "Завершить" → `endSession()`, "Продолжить" → `continueAfterCompletion()` ### Тесты - 202 теста, 0 фейлов - Покрыто: пейсинг, усталость, injection guard, crisis, session recovery, one-card UX, insights, snapshot prompts --- ## 7. Что не реализовано (план) ### 7.1 Structured session close [ПРИОРИТЕТ] **Что в плане** (§3, §4): Analyst возвращает структурированный объект — insight + sessionSummary + profileUpdates. **Что в коде**: `generateSessionInsight()` возвращает только одну строку-инсайт. - `profile.md` после сессии не обновляется новыми паттернами - `prompts.md` не обновляется - `sessions/YYYY-MM-DD.md` заполняется через `saveSessionToProfile()` где summary = **первые 3 реплики пользователя**, не аналитическое резюме **Решение**: `SessionCloseResult { insight: String, sessionSummary: String, profileUpdates: [String], promptUpdates: [String]? }`. Агент возвращает структуру → приложение раскладывает по файлам. ### 7.2 mood_delta → Orb visual feedback `AnalystJSON` → добавить `moodDelta: "declining" | "stable" | "improving"`. UI: declining = холодный синий/медленный пульс, stable = нейтральный, improving = amber/живой. ### 7.3 Планировщик как tool EventKit-интеграция как tool у Narrator — аналогично `search_sessions` / `read_profile`. Дизайн-решение отложено. ### 7.4 Retention protocol `[STAGE 2 — not in MVP]` При N пропущенных сессиях подряд → реактивационный флоу (§5). Отложено до после релиза MVP. Не входит в текущий спринт. ### 7.5 Android: TextEditor в Skip Fuse Проверить поддержку `TextEditor` + `fixedSize(horizontal:vertical:)` в Skip Fuse на Android. При необходимости заменить на `TextField(axis: .vertical)`. `.scrollContentBackground(.hidden)` уже огорожен `#if os(iOS)`. --- ## 8. Открытые вопросы - [ ] Планировщик — tool у Narrator (EventKit). Когда и как реализовывать? - [ ] mood_delta — 3 значений достаточно? (declining/stable/improving) - [ ] Android: `TextEditor` в Skip Fuse — нужна проверка на устройстве --- ## 9. Протокол агента — действующие правила ### NarratorBot systemPrompt содержит: - Legit-маркер (защита от injection false positive) - Injection guard директива - Off-topic директива (мета-вопросы → вернуть фокус) - Детекция усталости → передать в metadata для Analyst - Детекция настроения → `sentimentObservation` в metadata ### AnalystBot systemPrompt содержит: - Пейсинг-правила (turn_count, 4+ оборотов → оценка exhaustion, 7+ → принудительно) - Директива при усталости: `session_complete: true`, `questions: []` - Директива при мета-вопросах: не объяснять архитектуру, вернуть фокус - No-filepath директива: не упоминать пути файлов ### AnalystBot tools (read-only): - `search_sessions` — поиск по архиву - `read_session` — читать конкретную сессию - `get_index` — индекс всех сессий - `read_profile` — профиль пользователя - `list_files` / `read_file` — дополнительные файлы профиля --- ## 10. Хранение данных (on-device) Всё хранится через `ProfileManager` в `Documents/UserProfile/`: | Файл | Что содержит | Кто пишет | |------|-------------|-----------| | `profile.md` | Паттерны пользователя, наблюдения | `updateProfile()` — пока не вызывается автоматически после сессии | | `insights.md` | Инсайты по сессиям | `addInsight()` — пишется немедленно | | `prompts.md` | Промпты/темы пользователя | не обновляется автоматически | | `observations/Www.md` | Запись сессии за неделю | `appendSession()` | | `index.md` | Индекс сессий | `updateSessionIndex()` | **Расписание**: через системный календарь (EventKit), не отдельный файл. --- ## Связанные заметки - [[personal/projects/psychologist-app/overview]] - [[personal/projects/psychologist-app/character-design]] - [[personal/projects/psychologist-app/onboarding-ux]]