feat(secretary): reconcileTurn прокидывает step в результат

Task 3/5 плана. stampProvenance отдаёт фиксированную форму без step, поэтому
reconcileTurn оборачивает результат: finish() прикладывает parsed.step к
возвращаемому объекту (транзитно), при отсутствии — поля нет. Свод 111/111.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-06-23 09:28:14 +03:00
parent ff16d05ec3
commit 124dbeef95
2 changed files with 12 additions and 2 deletions
+4 -2
View File
@@ -275,13 +275,15 @@ export async function reconcileTurn({ proto, ex, turn, session, callModel, diag
catch (e) { report({ reason: 'model-threw', error: String((e && e.message) || e) }); return null; }
const parsed = parseReconcileResponse(typeof text === 'string' ? text : '');
if (!parsed) { report({ reason: 'bad-json' }); return null; } // кривой JSON — прежний протокол цел
const step = parsed.step || null;
const finish = (p) => { const out = collapseProtocol(p); return step ? { ...out, step } : out; };
const returned = collapseProtocol(parsed);
const guard = reconcileGuard(clean, returned);
if (guard.ok) return collapseProtocol(stampProvenance(clean, returned, turn, session));
if (guard.ok) return finish(stampProvenance(clean, returned, turn, session));
// Потеряны строки → НЕ выкидываем ход: возвращаем пропавшие старые строки на место (модель-агностично).
// Что модель уронила — хук вернул; что обновила (закрыла вопрос, добавила решение) — сохранено.
report({ reason: 'guard-restored', lost: guard.lost });
return collapseProtocol(stampProvenance(clean, restoreLostLines(clean, returned), turn, session));
return finish(stampProvenance(clean, restoreLostLines(clean, returned), turn, session));
}
/** Протокол к записи независимо от исхода reconcile: при успехе база — updated, при срыве —
+8
View File
@@ -164,6 +164,14 @@ describe('reconcileTurn', () => {
expect(out).toBeNull();
expect(n).toBe(1);
});
it('проброс step из ответа модели в результат; без step — поля нет', async () => {
const withStep = async () => '{ "subject":"дело", "decisions":[{"text":"A","struck":false}], "open":[{"text":"Q?","struck":true}], "will":[], "doneNext":[], "step":{"user":"u","assistant":"a"} }';
const r1 = await reconcileTurn({ proto, ex, turn: 5, session: 's1', callModel: withStep });
expect(r1.step).toEqual({ user: 'u', assistant: 'a' });
const noStep = async () => '{ "subject":"дело", "decisions":[{"text":"A","struck":false}], "open":[{"text":"Q?","struck":true}], "will":[], "doneNext":[] }';
const r2 = await reconcileTurn({ proto, ex, turn: 5, session: 's1', callModel: noStep });
expect(r2.step).toBeUndefined();
});
});
import { mergeTurnIntoProtocol, formatReconcileLogLine, restoreLostLines, collapseProtocol } from './secretary-reconcile.mjs';