Files
obsidian-vault/personal/projects/psychologist-app/session-design.md
T

613 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Session Design — AI Psychologist App"
aliases:
- "Дизайн сессии"
- "Session design"
- "архитектура сессий"
created: '2026-05-19'
updated: '2026-05-24'
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 сам управляет порядком.
### Структура данных от агента (JSON для UI)
```json
{
"questions": [
{
"id": 1,
"text": "Ты замечаешь, что откладываешь этот разговор?",
"type": "open"
},
{
"id": 2,
"text": "Что происходит внутри когда думаешь об этом?",
"type": "open"
}
],
"answered": [],
"session_id": "2026-05-19"
}
```
### Завершение батча вопросов → подбивка
Когда все вопросы в батче отвечены, UI передаёт агенту нумерованную подбивку:
```
1. [вопрос] → [ответ пользователя]
2. [вопрос] → [ответ пользователя]
```
Это входные данные для следующей итерации диалога (следующий батч вопросов или инсайт).
---
## 2. Открытие сессии
### Структура intro
1. **Персонаж-активация** (анимация, 1-2 сек) — без текста, пауза перед словами
2. **Короткое введение** (1-2 предложения максимум):
- Что происходит сегодня в целом (настроение агента, не диагноз)
- Без "Давай начнём с..." — сразу к делу
3. **"Крючки-активаторы"** из прошлых бесед:
- 1-2 элемента из profile.md / последних сессий
- Формат: карточка-напоминание ("В прошлый раз ты говорил о...")
- **Не перегружать**: максимум 2 крючка, не перечислять всю историю
- Если нет прошлых сессий — пропустить этот блок
4. **Переход к теме** — либо предложение агента, либо выбор пользователя
### Prompt агента для intro
Агент (Analyst) при старте сессии получает:
- `profile.md` — текущий профиль
- `sessions/[last 1-2].md` — последние сессии
- Задача: сформировать intro + max 2 крючка, без перечисления всего
---
## 3. Закрытие сессии
### Последовательность закрытия (3 этапа)
#### Этап 1 — Психолог обновляет документы (backend, не видно пользователю)
После завершения диалогового флоу Analyst:
- Пишет `sessions/YYYY-MM-DD.md` — запись сессии
- Обновляет `profile.md` — новые паттерны/наблюдения
- Формирует structured summary для Посредника
#### Этап 2 — Посредник: человечный итог (видно пользователю)
Посредник в режиме "простого AI-агента" — без аналитики, без психолого-языка:
- Тёплая, живая фраза от себя (не "подводя итог...")
- Примерный тон: "И от себя — это больше чем ничего. Ты на верном пути."
- **Не повторять** то что уже сказал психолог
- **Не хвалить** избыточно — anti-sycophancy принцип сохраняется
- Сказать когда следующая сессия: "До встречи в четверг" (из расписания пользователя)
#### Этап 3 — "One last thing" + завершение
- Агент задаёт открытый вопрос с вольным промптом:
"Хочешь что-то добавить?"
- **Визуально**: маленький текстовый input, **не акцентная** кнопка "Завершить"
- Этот экран не предполагает продолжения полноценной сессии
- Если пользователь что-то написал → агент может добавить это в заметки сессии, ответить коротко
- Если нажал "Завершить" без ввода → сессия закрывается
### Сценарий сдвига расписания
Если в "one last thing" (или на любом этапе закрытия) пользователь упоминает сдвиг даты/времени:
- Детектируется ключевыми словами (перенести, следующий раз, не смогу в четверг, и т.д.)
- Запускается **сценарий планировщика** (отдельный флоу)
- Беседа переходит в режим планировщика и **завершается в нём** (не возвращается в сессионный флоу)
- Результат: обновлённое расписание, подтверждение новой даты
---
## 4. Флоу сессии целиком
```
ОТКРЫТИЕ
├── Анимация персонажа (1-2 сек)
├── Intro: 1-2 предложения + max 2 крючка из истории
└── Выбор темы (пользователь или AI предлагает)
ДИАЛОГ
├── Analyst генерирует 1-2 вопроса (3-е лицо)
├── Narrator переформулирует → JSON карточек (1-е лицо)
├── UI раздаёт по одному
├── Пользователь отвечает → нумерованная подбивка
└── Следующий батч / инсайт
ИНСАЙТ
├── Analyst кристаллизует наблюдение (3-е лицо)
└── Narrator переформулирует для пользователя (1-е лицо)
ЗАКРЫТИЕ
├── [backend] Analyst пишет session summary, обновляет profile.md
├── Посредник: тёплая фраза + дата следующей сессии
├── "One last thing" (вольный промпт, неакцентное завершение)
│ ├── Написал что-то → агент добавляет в заметки, отвечает коротко
│ ├── Упомянул перенос → сценарий планировщика
│ └── Завершить (кнопка) → сессия закрыта
```
---
## 5. State Persistence & Session Recovery
### Сохранение стейта на каждом шаге
Каждый шаг сессии сохраняется немедленно — не в конце:
```
step_state {
session_id: "2026-05-19",
user_id: "...",
phase: "dialogue", // intro | questionnaire | dialogue | insight | closing
current_question_idx: 2,
answered: [ {q: "...", a: "..."}, ... ],
last_activity_ts: 1747612800
}
```
При следующем открытии приложения — автоматически восстанавливается точка, где пользователь остановился. Не начинает с начала.
### Заброшенная сессия (пользователь пропал посередине)
**Триггер**: last_activity_ts > 20 часов, сессия не завершена.
**Действие (backend cron)**:
1. Принудительно **завершить сессию** — агент пишет partial summary на основе того, что успело ответить
2. Обновить `profile.md` — даже неполные данные полезны
3. Пометить сессию как `completed_partial`
**Уведомление**: на следующий день **примерно в то же время** (± 30 мин от last_activity_ts предыдущих сессий) — push:
> "Мы не успели закончить в прошлый раз. Вернёмся?"
**Retention protocol** (Stage 2): при N пропущенных сессиях подряд — запускается отдельный флоу "возвращение пользователя" с мягким реактивационным сообщением. Детали — в будущем документе.
---
## 6. Открытые вопросы
- [ ] Как именно Narrator детектирует что вопросы в 3-м лице и нужно переформулировать? Prompt rule или регекс-паттерн?
- [ ] Сценарий планировщика — отдельный агент или встроен в посредника?
- [ ] Как хранить расписание сессий: отдельный `schedule.md` в профиле?
- [ ] Что если "one last thing" раскрывает новую тему — прерывать или кратко зафиксировать?
---
## 7. Пробелы реализации (план есть — кода нет)
> Обнаружено в ходе QA сессий. Дата: 2026-06-02.
### 7.1 Пейсинг сессии — НЕ РЕАЛИЗОВАН
**Что в плане**: ДИАЛОГ → ИНСАЙТ → ЗАКРЫТИЕ — явный флоу.
**Что в коде**: агент задаёт вопросы бесконечно. Нет счётчика turns. Нет сигнала
`session_complete`. Нет правил пейсинга в системном промпте AnalystBot.
**Симптом**: сессия 2026-06-02 — подзатянулась, пользователь устал раньше чем
агент инициировал закрытие.
### 7.2 State Persistence & Session Recovery — НЕ РЕАЛИЗОВАН
**Что в плане** (Section 5): `step_state` на каждом шаге, автовосстановление точки
входа при перезапуске приложения, partial summary при заброшенной сессии.
**Что в коде**: ничего. Каждая сессия начинается с нуля. `startSession()` создаёт
новый `Session()` без проверки незавершённой.
**Симптом**: нажатие X во время активной сессии — единственный выход.
`dismissSession()``cancelSession()` сохраняет сессию, но без инсайта.
`endSession()` (полный флоу с инсайтом) из UI недоступен — кнопки нет.
### 7.3 Кнопка "Завершить сессию" — ОТСУТСТВУЕТ В UI
**Что должно быть**: явная кнопка `endSession()` в диалоговом экране.
**Что в коде**: SessionView имеет только X (xmark) → `dismissSession()`.
`endSession()` существует в SessionViewModel, но не вызывается нигде из UI.
**Следствие**: пользователь не может корректно завершить сессию с инсайтом через UI
пока агент не инициирует закрытие (которого тоже нет).
### 7.4 Агент раскрывает внутренние пути файлов
**Что произошло**: при завершении сессии AnalystBot написал в ответе:
"Сохраняю в personal/psychology/observations/..." — пользователю виден внутренний
путь хранилища.
**Проблемы**:
1. Пользователю не нужно видеть файловые пути — это внутренняя деталь
2. Путь в ответе агента не соответствует реальному (on-device: `Documents/UserProfile/`
vs vault: `personal/psychology/`)
3. Агент не должен сам "сохранять" — сохранение делает приложение через
`ProfileManager.saveSessionToProfile()`. Агент должен либо ничего не говорить
о сохранении, либо вызвать `save_session` API-метод приложения.
### 7.5 Отсутствует save_session app API tool
**Текущие tools AnalystBot**: только read-операции (`search_sessions`, `read_session`,
`get_index`, `read_profile`). Нет `save_session` или `update_profile` tools.
**Следствие**: агент не может явно инициировать сохранение. Сохранение происходит
автоматически при вызове `cancelSession()` / `endSession()` из кода приложения.
Агент об этом не знает → упоминает пути в ответах как бы "для информирования".
---
## 8. Пейсинг сессии — План реализации
### 8.1 Модель данных: session_complete сигнал
**`AnalystBot.swift`** — добавить поле в `AnalystJSON` и `AnalystResponse`:
```swift
struct AnalystJSON: Codable {
let questions: [String]
let observation: String?
let crisisSignal: Bool
let suggestedInsight: String?
let sessionComplete: Bool // ← новое поле
}
```
**`SessionManager.swift`** — передавать `turnCount` в `analyze()`:
```swift
func analyze(narratedText: String, turnCount: Int) async -> AnalystResponse
```
### 8.2 Правила пейсинга в SystemPrompt AnalystBot
Добавить блок в конец системного промпта:
```
Управление длиной сессии:
- Параметр turn_count = количество завершённых оборотов (вопрос + ответ)
- После 4+ оборотов: если тема исчерпана, пользователь пришёл к пониманию
или повторяется — установи session_complete: true
- Если открылась новая важная тема — продолжай (session_complete: false)
- После 7+ оборотов: session_complete: true независимо от темы
- session_complete: true = предложение закрыть сессию, не принудительное завершение
```
### 8.3 Новый кейс ProcessingResult
```swift
enum ProcessingResult {
case questions([String], observation: String?)
case readyToClose([String]?, observation: String?) // вопросы опциональны
case crisis(CrisisDetectionResult)
case error(String)
}
```
### 8.4 SessionManager — обработка сигнала
В `processUserInput()` после получения `analystResponse`:
- `analystResponse.sessionComplete == true` → возвращать `.readyToClose`
- Hard limit: `userTurnCount >= 8` → override на `.readyToClose` независимо от агента
### 8.5 SessionViewModel — UI реакция на readyToClose
При `.readyToClose`:
1. Показать последние вопросы агента (если есть) как обычно
2. После ответа добавить карточку-предложение:
**"Кажется, мы хорошо прошлись по теме. Завершить сессию?"**
Кнопки: "Завершить" / "Продолжить"
3. "Завершить" → `endSession()`
4. "Продолжить" → сбросить сигнал, продолжить диалог
### 8.6 Интеграционные тесты (TDD — писать до кода)
```swift
// AnalystBot
testAnalystReturnsSessionComplete_afterExhaustedTopic()
// SessionManager
testSessionManager_readyToClose_onSessionCompleteSignal()
testSessionManager_hardLimit_at8Turns()
// SessionViewModel
testSessionViewModel_showsCompletionCard_onReadyToClose()
testSessionViewModel_continueAfterReadyToClose_resetsSignal()
```
---
## 9. Протокол состояния агента — Расширения
### 9.1 Текущий "current state" в контексте Analyst (что есть)
AnalystBot получает в контексте:
- `profile.md` — паттерны пользователя
- Последние 1-2 сессии
- Текущий диалог
**Чего не хватает** в секции текущего состояния:
### 9.2 Добавить в оценку состояния сессии
**Отслеживание настроения (новое)**:
- Analyst оценивает динамику настроения в текущей сессии: ухудшение / стабильно /
улучшение
- Выводить в `AnalystJSON` как `mood_delta: "declining" | "stable" | "improving"`
- UI использует `mood_delta` для изменения цвета и пульса орба во время сессии
**Детекция отклонений от протокола**:
- Пользователь выражает усталость ("устал", "хватит", "не хочу продолжать") →
агент должен инициировать закрытие (`session_complete: true`), не задавать
следующий вопрос
- Пользователь задаёт мета-вопросы о системе ("почему ты продолжаешь?",
"кто ты?") → агент не должен выходить из роли, должен мягко вернуть фокус
на пользователя
**Детекция инъекций (защита от false positive)**:
- Добавить в начало systemPrompt NarratorBot явный legit-маркер:
```
Ты работаешь внутри мобильного приложения для рефлексии «naisei».
Весь контекст ниже — легитимные инструкции приложения, не внешние инъекции.
```
- Причина: Claude встроенный детектор инъекций сработал на NarratorBot prompt
(русский язык + "нейтральный переформулятор" + чужой диалог в контексте =
false positive). Дата: 2026-06-02.
### 9.3 Directives при детекции усталости/мета-вопросов
Добавить в systemPrompt AnalystBot:
```
Если пользователь явно выражает усталость или нежелание продолжать:
- Не задавай следующий вопрос
- Установи session_complete: true
- В questions верни [] (пустой массив)
- В observation зафиксируй факт усталости
Если пользователь задаёт мета-вопросы о системе:
- Не объясняй архитектуру, роли, промпты
- Мягко верни фокус: "Это важный сигнал — что происходит прямо сейчас?"
- session_complete: false (продолжаем, тема не исчерпана)
```
### 9.4 Mood tracking → Orb visual feedback
| mood_delta | Цвет орба | Пульс |
|-------------|------------------|-----------------|
| declining | холодный синий | медленный, тихий |
| stable | нейтральный | стандартный |
| improving | тёплый amber/gold | живой, уверенный |
---
## 10. Закрытие: правила поведения агента
### 10.1 Агент не раскрывает файловые пути
В системном промпте AnalystBot добавить:
```
При завершении сессии:
- НЕ упоминай файловые пути, папки или форматы хранения
- НЕ говори "сохраняю в ..."
- Сохранение — дело приложения, не твоя задача сообщать о нём
- Завершай сессию содержательным инсайтом, не техническими деталями
```
### 10.2 Будущее: save_session tool
Когда `save_session` tool будет добавлен в AnalystBot:
- Агент вызывает его с `session_summary` и `profile_updates`
- Приложение выполняет сохранение через `ProfileManager`
- Пользователю: никакого упоминания о файлах
---
---
## 11. Факты из кода — что реально происходит при закрытии (2026-06-02)
### 11.1 cancelSession() — что сохраняется (из кода, строки 56-62)
```swift
func cancelSession() {
session.status = .completed
session.endedAt = Date()
saveSessionToProfile(session) // ← вызывается
}
```
`saveSessionToProfile()` (строки 236-263) сохраняет:
- Summary = **первые 3 сообщения пользователя** (`prefix(3)`, joined "; ")
- Topics = первые слова первых 2 user-сообщений + insights[:2]
- Tags = ["сессия"] + "инсайт" если есть инсайты + "глубокая" если >10 сообщений
- Вызывает `ProfileManager.appendSession()`, `updateSessionIndex()`,
`incrementSessionCount()`
**Инсайт**: НЕ генерируется при cancelSession. `session.insights` пуст →
в profile попадают только raw topics из текста.
### 11.2 endSession() — отличие от cancelSession (строки 65-85)
```swift
func endSession() async -> String? {
// + генерирует finalInsight если visibleMessages.count >= 3
finalInsight = await generateSessionInsight(session)
session.insights.append(insight)
saveSessionToProfile(session) // теперь insights не пустой
return finalInsight
}
```
`generateSessionInsight()` (строки 87-116):
- Берёт только `userTexts.prefix(500)` — первые 500 символов всех user-сообщений
- **Баг**: для длинной сессии инсайт генерируется по урезанным данным
- Требует минимум 3 видимых сообщений
### 11.3 Кто реально записал vault-файл 2026-W22.md
`e25dee9 [2026-06-02]` — коммит Eagle (Claude Code на Mac), сообщение:
> "Восстановлены из лога ai-proxy. Предыдущая запись содержала только последние 2 обмена."
**Факт**: приложение на телефоне сохраняет ТОЛЬКО в on-device хранилище
(`Documents/UserProfile/`). Vault-файл записывается отдельно — вручную Eagle или
через Claude Code CLI с полным доступом к ФС мака.
### 11.4 Проблема безопасности: ai-proxy имеет полный доступ к ФС мака
**Что произошло**: Claude Code (ai-proxy на маке) в ходе сессии написал полный
vault-документ напрямую в `~/obsidian/personal/psychology/observations/2026-W22.md`.
**Почему это проблема**:
1. Агент внутри приложения (AnalystBot) через ai-proxy имеет косвенный доступ
к ФС мака — без явного app API call
2. Vault-путь `personal/psychology/observations/` стал известен агенту из контекста
и был упомянут в ответе — утечка internal path через LLM output
3. На телефоне должен быть только on-device path — vault-запись должна идти через
отдельный sync-механизм, а не через агента с ФС-доступом
**Правильная архитектура**:
- Приложение сохраняет on-device через ProfileManager
- Отдельный sync job (scheduled, не real-time) экспортирует сессии в vault
- Агент **никогда** не знает vault-пути — только app-internal storage paths
### 11.5 Баг: generateSessionInsight использует только prefix(500)
Строка 101: `userTexts.prefix(500)` — для сессии из 15+ обменов это первые 1-2 ответа.
Инсайт по длинной сессии будет неполным / нерелевантным.
**Фикс**: передавать полный диалог или summary всех user-сообщений с truncation
по tokens, а не по символам начала.
### 11.6 Session Recovery — подтверждено: НЕ реализовано
`startSession()` (строка 50-53):
```swift
func startSession() {
let session = Session() // всегда новый, без проверки незавершённой
currentSession = session
}
```
В плане (Section 5) описан `step_state` и автовосстановление — в коде его нет.
---
---
## 12. План работ — актуальный (обновлено 2026-06-02)
### 12.1 Реализовано ✅
- `AnalystResponse.sessionComplete: Bool` + AnalystJSON backward-compatible
- `ProcessingResult.readyToClose([String]?, observation:)`
- `SessionManager.userTurnCount`, `consecutiveShortAnswerCount`, hard limit 8 ходов
- `SessionManager.continueSession()`, `detectIncompleteSession()`, `resumeOrStart()`
- `FatigueContext`, `IncompleteSessionState`, `SessionResumeResult`
- NarratorBot: legit-маркер, injection guard, off-topic директива, детекция настроения/усталости
- AnalystBot: turnCount/startTime/duration/messageCount/fatigueCtx → user message; пейсинг-правила в systemPrompt
- SessionViewModel: `showCompletionPrompt`, `showCompletionCard()`, `continueAfterCompletion()`
- CardData: `isCompletion: Bool`
- 182 теста, 0 фейлов
### 12.2 Ближайшие задачи
#### A. UI-связка карточки завершения (5 строк)
`onQuickReply` в SessionView: если `card.isCompletion` → "Завершить" → `endSession()`, "Продолжить" → `continueAfterCompletion()`. Без этого карточка есть, но кнопки не работают.
#### B. Session Recovery при входе (~20 строк)
Заменить `startSession()` на `resumeOrStart()` при открытии SessionView. При `.resumed` — восстановить карточки из `session.messages` (видимые .analyst сообщения). При `.startedAfterPartialSummary` — показать "В прошлый раз..." первой карточкой.
#### C. UX: одна карточка + growing input [НОВОЕ]
- Одна карточка одновременно на экране (не список)
- При открытии клавиатуры карточка уходит вверх над орбом, не скрывается
- Поле ввода: `TextEditor` с `axis: .vertical`, растёт по мере набора, word-wrapped
#### D. generateSessionInsight — полный анализ с инструментами [ОБЯЗАТЕЛЬНО]
**Проблема:** сейчас `generateSessionInsight()` — простой `introService.complete()` (Haiku, один вызов без инструментов, с обрезкой контента). Никакой связи с историей сессий.
**Требование:** агент анализирует ВСЮ сессию, без программной обрезки, с доступом к прошлым сессиям для сравнения паттернов.
**Реализация:**
- Заменить `introService.complete()` на `analystBot.completeWithTools()` (те же инструменты: `search_sessions`, `read_session`, `get_index`, `read_profile`)
- Передавать полный диалог текущей сессии (все сообщения user + analyst)
- Промпт: синтез инсайта + сравнение с паттернами из прошлых сессий
- Контекстного окна Sonnet (200k) достаточно для любой бытовой сессии без chunking
- Chunking как крайний fallback только если диалог > ~150k символов
**Что меняется в data flow:**
```
Было: generateSessionInsight → Haiku.complete(prefix_500_chars) → строка
Стало: generateSessionInsight → Sonnet.completeWithTools(full_dialogue + tools) → строка
↳ может вызвать search_sessions("паттерн X") → сравнить с прошлым
↳ может вызвать read_profile() → уточнить контекст
↳ возвращает инсайт с учётом всей истории
```
#### E. insights.md — финальный инсайт не записывается [БАГ]
`saveSessionToProfile()` пишет в `observations/Www.md` и `index.md`, но **не вызывает** `ProfileManager.appendInsight()`. Итог: `insights.md` (который читает AnalystBot через `read_profile`) не обновляется после сессии. Добавить одну строку в `saveSessionToProfile()`.
#### F. insights.md — suggestedInsight по ходу диалога [БАГ]
`suggestedInsight` от аналитика добавляется в `session.insights` (в памяти), но в `insights.md` не пишется до конца сессии. Если сессия прервана через X — инсайт теряется. Писать немедленно при получении.
### 12.3 Следующая итерация
- mood_delta → Orb visual feedback (Section 9.4)
- save_session app API tool для агента (Section 7.5)
- Vault sync архитектура: on-device → vault отдельным job (Section 11.4)
- Retention protocol при пропущенных сессиях (Section 5)
- Android Skip совместимость новых типов
---
## 6. Открытые вопросы
- [ ] Как именно Narrator детектирует что вопросы в 3-м лице и нужно переформулировать? Prompt rule или регекс-паттерн?
- [ ] Сценарий планировщика — отдельный агент или встроен в посредника?
- [ ] Как хранить расписание сессий: отдельный `schedule.md` в профиле?
- [ ] Что если "one last thing" раскрывает новую тему — прерывать или кратко зафиксировать?
- [ ] save_session tool — когда добавлять, как передавать structured summary?
- [ ] mood_delta — достаточно ли 3 значений, нужна ли шкала -2..+2?
- [ ] Vault sync архитектура: когда/как экспортировать on-device сессии в vault,
чтобы агент не имел прямого доступа к ФС мака?
## Связанные заметки
- [[personal/projects/psychologist-app/overview]]
- [[personal/projects/psychologist-app/character-design]]
- [[personal/projects/psychologist-app/onboarding-ux]]