feat(secretary): reconcile возвращает step{user,assistant}
Task 1/5 плана. parseReconcileResponse читает поле step (суть хода) →
{user,assistant} или null; buildReconcilePrompt просит это поле (правило 8).
Свод секретаря 108/108.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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 категорий + стабильная тема', () => {
|
||||
|
||||
Reference in New Issue
Block a user