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:
@@ -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` / реестр скрытых вопросов.
|
||||
Reference in New Issue
Block a user