diff --git a/tools/secretary-reconcile.mjs b/tools/secretary-reconcile.mjs index 33c8faf..5df00bf 100644 --- a/tools/secretary-reconcile.mjs +++ b/tools/secretary-reconcile.mjs @@ -15,6 +15,10 @@ export function buildReconcilePrompt({ protocol = {}, lastExchange = {}, remark '5. Игнорируй служебный шум (coverage, экономия, штатный, механика хуков/стены).', '6. "why" — реальное обоснование; "subject" — стабильная суть всего дела.', '7. Заполняй "alternatives" (что рассматривали и отвергли) и "consequences" (последствия/цена/риски).', + '8. ДОПОЛНИТЕЛЬНО верни поле "step": {"user":"<суть: что юзер хотел/спросил>",', + ' "assistant":"<что ассистент сделал/выяснил/решил/предложил + ключевые находки>"} —', + ' сжатая СУТЬ текущего хода без воды (вежливость/повторы убери; длина по содержанию;', + ' факты не выдумывай; инструменты НЕ перечисляй — их подставит система).', ].join('\n'); const sec = (name, arr) => `${name}:\n` + ((arr || []).map((e) => ` - ${e.struck ? '[зачёркнуто] ' : ''}${e.text}${e.why ? ' — ' + e.why : ''}`).join('\n') || ' (пусто)'); @@ -43,6 +47,12 @@ export function parseReconcileResponse(llmText) { if (!parsed || typeof parsed !== 'object') return null; const list = (x) => (Array.isArray(x) ? x : []); const ent = (e) => ({ text: String(e && e.text || ''), struck: !!(e && e.struck) }); + const parseStep = (s) => { + if (!s || typeof s !== 'object') return null; + const u = typeof s.user === 'string' ? s.user.trim() : ''; + const a = typeof s.assistant === 'string' ? s.assistant.trim() : ''; + return (u || a) ? { user: u, assistant: a } : null; + }; return { subject: typeof parsed.subject === 'string' ? parsed.subject.trim() : '', decisions: list(parsed.decisions).map((e) => ({ ...ent(e), why: (e && e.why) || null })), @@ -51,6 +61,7 @@ export function parseReconcileResponse(llmText) { will: list(parsed.will).map(ent), open: list(parsed.open).map(ent), doneNext: list(parsed.doneNext).map((e) => ({ ...ent(e), done: !!(e && e.done) })), + step: parseStep(parsed.step), }; } diff --git a/tools/secretary-reconcile.test.mjs b/tools/secretary-reconcile.test.mjs index 9fb3da9..e681c27 100644 --- a/tools/secretary-reconcile.test.mjs +++ b/tools/secretary-reconcile.test.mjs @@ -14,6 +14,12 @@ describe('parseReconcileResponse', () => { expect(parseReconcileResponse('не json')).toBeNull(); expect(parseReconcileResponse('')).toBeNull(); }); + it('читает step{user,assistant}; пустой/кривой → null', () => { + const out = parseReconcileResponse('{ "subject":"S", "step":{ "user":" хотел X ", "assistant":"сделал Y" } }'); + expect(out.step).toEqual({ user: 'хотел X', assistant: 'сделал Y' }); + expect(parseReconcileResponse('{ "subject":"S" }').step).toBeNull(); + expect(parseReconcileResponse('{ "subject":"S", "step":{} }').step).toBeNull(); + }); }); describe('reconcileGuard', () => { @@ -106,6 +112,11 @@ describe('buildReconcilePrompt', () => { expect(user).toContain('ответ на Q'); expect(user).toContain('ВЕРНИ X'); }); + it('просит поле step (суть хода)', () => { + const { system } = buildReconcilePrompt({ protocol: { decisions: [], open: [], will: [], doneNext: [] }, lastExchange: {} }); + expect(system.toLowerCase()).toContain('step'); + expect(system.toLowerCase()).toContain('суть'); + }); }); describe('reconcile — 9 категорий + стабильная тема', () => {