docs(secretary): спека — секретарь формулирует строку «Хода» (Слой 2)

Витрина «Шаги» сейчас режется детерминированно (firstSentence: слепой обрез
130 знаков посреди слова, пример — ход 12 «протокола»). Дизайн: суть хода
формулирует тот же секретарь-модель — одно поле step{user,assistant} в том же
reconcile-вызове (без лишней платы). Инструменты и ссылка на Слой 1 остаются
детерминированными (хук, не LLM). Фолбэк на сбой; модельный текст переживает
выключение (слияние, не перезатир buildStepsFromRaw). Слой 1 не трогаем.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-23 09:09:09 +03:00
parent 4d7f355dca
commit 41105b615e
@@ -0,0 +1,109 @@
# Секретарь формулирует строку «Хода» (Слой 2 — витрина «Шаги»)
**Дата:** 2026-06-23 · **Статус:** дизайн на ревью · **Автор:** контроллер + владелец
## Цель
Сделать раздел **«Шаги (Слой 1)»** протокола читаемой летописью «как мы до этого
дошли», которую можно отдать другому агенту — и он поймёт ход работы, **не ныряя
в Слой 1**. Сейчас строка хода строится детерминированно (`buildStepLine`
`firstSentence`): для длинных реплик без точек это слепой обрез по 130 знаков
посреди слова (пример — ход 12 дела «протокол»: «…реально скармливаем наставн…»,
второй вопрос юзера потерян). Слой 1 (сырьё) остаётся как было — это страховка на
случай сбоя секретаря; меняем только витрину Слоя 2.
## Решение (суть)
Строку-суть хода формулирует **тот же секретарь-модель**, что уже пишет протокол
(вызов `callModel` на `SECRETARY_LLM_KEY` в stop-hook). Модель уже получает весь
протокол + текущий обмен — добавляем в её JSON-ответ **одно поле `step`**. Лишнего
вызова и платы нет. Детерминированным остаётся только то, чему модель доверять
нельзя: список реальных инструментов хода и ссылка на Слой 1 — их дописывает хук.
## Контракты по файлам
### 1. `tools/secretary-reconcile.mjs`
- **`buildReconcilePrompt`** — в system добавить правило и попросить поле `step`:
> `step` — суть ТЕКУЩЕГО хода без воды: `{ "user": "<что юзер хотел/спросил>",
> "assistant": "<что ассистент сделал/выяснил/решил/предложил + ключевые
> находки>" }`. Убирай воду, вежливость, повторы; длина по содержанию (короткий
> ход — короче). Факты не выдумывай. Инструменты НЕ перечисляй — их подставит
> система.
- **`parseReconcileResponse`** — читать `parsed.step`; нормализовать в
`{ user: string, assistant: string }` или `null` (если поля нет/кривое).
- **`reconcileTurn`** — приложить `step` к возвращаемому объекту (`stampProvenance`
отдаёт фиксированную форму без `step`, поэтому `{ ...stampProvenance(...), step }`).
При срыве модели (`null`) — `step` отсутствует, дальше работает фолбэк.
`step`**транзитное** поле результата reconcile: его потребляет stop-hook для
строки шага и **в `protocol.json` НЕ сохраняет** (хук срезает перед записью). Не
часть схлопываемых корзин; `collapseProtocol` его не трогает.
### 2. `tools/secretary-layer1.mjs`
- **`buildStepLine`** — добавить необязательный вход `essence = { user, assistant }`.
Если `essence` задан и непустой — `u = essence.user`, `a = essence.assistant`
(лёгкая чистка пробелов, без `firstSentence`/обреза). Если нет — прежнее
поведение (`firstSentence`/`sysLabel`) как фолбэк. `делал: <инструменты>`
(дедуп реальных tool-имён) и шаблон `Ход N — я: … · ты: … · делал: …`
общие для обоих путей.
- **`buildStepsFromRaw`** — НЕ менять формулировку (остаётся детерминированной:
это путь восстановления из сырья). Меняется только то, КАК его зовут (см. п. 3).
### 3. `tools/secretary-stop-hook.mjs`
- На каждом ходу: если `updated.step` есть — строить шаг через
`buildStepLine({ turn, actions, essence: updated.step })`; иначе — прежний
детерминированный `buildStepLine({ turn, user: ex.user, assistant: ex.assistant,
actions })`. Результат — в `step.text`, дальше как сейчас
(`mergeTurnIntoProtocol` пишет шаг ВСЕГДА — гарантия целостности «Шагов» цела).
### 4. `tools/secretary-prompt-hook.mjs` (ветка `off`, нарезка)
- Сейчас: `proto.steps = buildStepsFromRaw(raw, session)` — пересобирает ВСЕ шаги
из сырья, **затирая модельные формулировки**. Цель этого кода — заполнить ходы,
где секретарь был выключен (без пропусков), а не переписать хорошие.
- Меняем на **слияние по ходу**: брать существующий `proto.steps[turn].text`, а из
`buildStepsFromRaw` достраивать ТОЛЬКО ходы, которых в `proto.steps` нет. Модельный
текст переживает выключение/нарезку; ссылки `ходы/turn-N.log` проставляются как
сейчас (`prepareTurnFiles`).
## Поток данных
```
Stop (ход N, секретарь ON)
→ reconcileTurn(): модель возвращает протокол + step{user,assistant}
→ stop-hook: step есть? buildStepLine(essence=step) ИНАЧЕ buildStepLine(firstSentence)
→ + «делал: <реальные tools>» + ссылка Слой 1 (детерминированно, хук)
→ mergeTurnIntoProtocol: шаг записан ВСЕГДА
→ collapseProtocol → write
UserPrompt «выключи секретаря» (off)
→ слияние: существующие модельные шаги + достройка пропущенных ходов из сырья
→ нарезка ходы/turn-N.log + ссылки → write
```
## Гарантии и фолбэки
- **Шаг есть всегда** (сбой/нет ключа/кривой JSON → детерминированная короткая
строка). При мёртвом секретаре протокол и так не ведётся — Слой 1 страхует.
- **Инструменты и ссылка — факт, не модель** (хук, не LLM): нет галлюцинаций tool-имён.
- **Модельный текст переживает выключение/нарезку** (слияние, не перезатир).
- **Слой 1 без изменений** — сырьё пишется как было (страховка/восстановление).
## Тесты (TDD)
- `parseReconcileResponse`: читает `step{user,assistant}`; нет/кривой → `step` null.
- `buildStepLine`: с `essence` берёт модельный текст дословно + дописывает
`делал: <tools>`; без `essence` — прежний `firstSentence`-фолбэк (старые тесты целы).
- `reconcileTurn`: при ответе модели со `step``result.step` проброшен; при срыве — нет.
- Слияние на `off`: существующий модельный `text` сохранён; пропущенный ход достроен
из сырья; ссылки `ходы/turn-N.log` проставлены.
## Не-цели (YAGNI)
- Не трогаем Слой 1 (формат сырья, нарезку файлов).
- Не переформулируем задним числом прошлые ходы (модель видит только текущий обмен).
- Не добавляем отдельный вызов/агента — только поле в существующем reconcile-вызове.
- Не меняем 6 корзин / `collapseProtocol` / реестр скрытых вопросов.