feat(secretary): редактору обмен = последние 7 ходов целиком (не 7 действий)
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
// Секретарь-«редактор»: модель правит весь протокол, хук сторожит потери (спека reconcile).
|
||||
|
||||
// Сколько ПОСЛЕДНИХ действий хода подавать редактору (анти-раздувание: длинный ход с десятками
|
||||
// Read не возим целиком). Держим в синхроне с MAX_EXCHANGE_ACTIONS из secretary-harvest.mjs.
|
||||
const MAX_EXCHANGE_ACTIONS = 7;
|
||||
// Сколько ПОСЛЕДНИХ ходов спана подавать редактору целиком (юзер+ассистент+ВСЕ действия хода).
|
||||
const MAX_EXCHANGE_TURNS = 7;
|
||||
|
||||
/** Запрос к модели-редактору: весь протокол + обмен → весь обновлённый протокол. */
|
||||
export function buildReconcilePrompt({ protocol = {}, lastExchange = {}, remark = null } = {}) {
|
||||
@@ -36,11 +35,18 @@ export function buildReconcilePrompt({ protocol = {}, lastExchange = {}, remark
|
||||
].join('\n');
|
||||
const sec = (name, arr) => `${name}:\n` + ((arr || []).map((e) =>
|
||||
` - ${e.struck ? '[зачёркнуто] ' : ''}${e.text}${e.why ? ' — ' + e.why : ''}`).join('\n') || ' (пусто)');
|
||||
const allActs = lastExchange.actions || [];
|
||||
const shownActs = allActs.slice(-MAX_EXCHANGE_ACTIONS);
|
||||
const actsHead = allActs.length > shownActs.length ? ` (показаны последние ${shownActs.length} из ${allActs.length} действий)\n` : '';
|
||||
const acts = actsHead + ((shownActs.map((a) =>
|
||||
` • ${a.tool} in=${a.input ?? ''}${a.result != null ? `\n → ${String(a.result).replace(/\n/g, '\n ')}` : ''}`).join('\n')) || '—');
|
||||
const renderActs = (acts) => ((acts || []).map((a) =>
|
||||
` • ${a.tool} in=${a.input ?? ''}${a.result != null ? `\n → ${String(a.result).replace(/\n/g, '\n ')}` : ''}`).join('\n')) || ' —';
|
||||
let exchangeText;
|
||||
if (Array.isArray(lastExchange.turns) && lastExchange.turns.length) {
|
||||
const all = lastExchange.turns;
|
||||
const shown = all.slice(-MAX_EXCHANGE_TURNS);
|
||||
const head = all.length > shown.length ? `(показаны последние ${shown.length} из ${all.length} ходов)\n\n` : '';
|
||||
exchangeText = head + shown.map((t) =>
|
||||
`[ХОД ${t.turn}]\n[ЮЗЕР]: ${t.user || ''}\n[АССИСТЕНТ]: ${t.assistant || ''}\n[ДЕЙСТВИЯ]:\n${renderActs(t.actions)}`).join('\n\n');
|
||||
} else {
|
||||
exchangeText = `[ЮЗЕР]: ${lastExchange.user || ''}\n[АССИСТЕНТ]: ${lastExchange.assistant || ''}\n[ДЕЙСТВИЯ]:\n${renderActs(lastExchange.actions)}`;
|
||||
}
|
||||
const knowLines = ((protocol.knowledge || []).map((e) => ` - ${e.text}${e.ref ? ' — ' + e.ref : ''}`).join('\n')) || ' (пусто)';
|
||||
const user = [
|
||||
`Тема дела: ${protocol.subject || '(нет)'}`,
|
||||
@@ -48,10 +54,8 @@ export function buildReconcilePrompt({ protocol = {}, lastExchange = {}, remark
|
||||
sec('Решения', protocol.decisions), sec('Альтернативы', protocol.alternatives),
|
||||
sec('Последствия', protocol.consequences), sec('Воля', protocol.will),
|
||||
sec('Открытые', protocol.open), sec('Сделано', protocol.doneNext),
|
||||
'', 'Последний обмен:',
|
||||
`[ЮЗЕР]: ${lastExchange.user || ''}`,
|
||||
`[АССИСТЕНТ]: ${lastExchange.assistant || ''}`,
|
||||
`Действия (с содержимым):\n${acts}`,
|
||||
'', 'Последние ходы (обмен):',
|
||||
exchangeText,
|
||||
remark ? `\nЗАМЕЧАНИЕ (исправь и верни весь протокол):\n${remark}` : '',
|
||||
'', 'Верни ВЕСЬ обновлённый протокол как JSON.',
|
||||
].join('\n');
|
||||
|
||||
@@ -125,13 +125,24 @@ describe('buildReconcilePrompt', () => {
|
||||
expect(user).toContain('{"f":"x"}');
|
||||
expect(user).toContain('СОДЕРЖИМОЕ');
|
||||
});
|
||||
it('Последний обмен — только последние 7 действий с пометкой', () => {
|
||||
const actions = Array.from({ length: 10 }, (_, i) => ({ tool: 'Read', input: `f${i}`, result: `R${i}` }));
|
||||
const { user } = buildReconcilePrompt({ protocol: { decisions: [], open: [], will: [], doneNext: [] }, lastExchange: { user: 'u', assistant: 'a', actions } });
|
||||
expect(user).toContain('f9');
|
||||
expect(user).toContain('f3');
|
||||
expect(user).not.toContain('f2');
|
||||
expect(user).toMatch(/последние 7 из 10/);
|
||||
it('обмен редактора — последние 7 ходов целиком, действия не урезаны', () => {
|
||||
const turns = Array.from({ length: 9 }, (_, i) => ({
|
||||
turn: i + 1, user: `u${i + 1}`, assistant: `a${i + 1}`,
|
||||
actions: [{ tool: 'Read', input: `fA${i + 1}`, result: 'r' }, { tool: 'Read', input: `fB${i + 1}`, result: 'r' }],
|
||||
}));
|
||||
const { user } = buildReconcilePrompt({ protocol: { decisions: [], open: [], will: [], doneNext: [] }, lastExchange: { turns } });
|
||||
expect(user).toMatch(/последние 7 из 9 ходов/);
|
||||
expect(user).toContain('[ХОД 9]');
|
||||
expect(user).toContain('[ХОД 3]');
|
||||
expect(user).not.toContain('[ХОД 2]');
|
||||
expect(user).toContain('fA9');
|
||||
expect(user).toContain('fB9');
|
||||
});
|
||||
it('обмен редактора — фолбэк на единый блок, если turns нет', () => {
|
||||
const { user } = buildReconcilePrompt({ protocol: { decisions: [], open: [], will: [], doneNext: [] }, lastExchange: { user: 'спросил', assistant: 'ответил', actions: [{ tool: 'Read', input: 'x', result: 'СОДЕРЖ' }] } });
|
||||
expect(user).toContain('спросил');
|
||||
expect(user).toContain('ответил');
|
||||
expect(user).toContain('СОДЕРЖ');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -52,5 +52,6 @@ export function assembleSpan(rawText, { start, end }) {
|
||||
const startTurn = parsed.find((p) => p.turn === start) || parsed[0] || {};
|
||||
const assistant = parsed.map((p) => p.assistant).filter(Boolean).join('\n');
|
||||
const actions = parsed.flatMap((p) => p.actions);
|
||||
return { user: startTurn.user || '', assistant, actions };
|
||||
const turns = parsed.map((p) => ({ turn: p.turn, user: p.user, assistant: p.assistant, actions: p.actions }));
|
||||
return { user: startTurn.user || '', assistant, actions, turns };
|
||||
}
|
||||
|
||||
@@ -76,4 +76,11 @@ describe('assembleSpan', () => {
|
||||
expect(ex.user).toBe('настоящий промпт');
|
||||
expect(ex.actions).toHaveLength(1);
|
||||
});
|
||||
it('возвращает по-ходовый список turns (для редактора)', () => {
|
||||
const ex = assembleSpan(raw, { start: 3, end: 4 });
|
||||
expect(ex.turns).toEqual([
|
||||
{ turn: 3, user: 'настоящий промпт', assistant: 'первый ответ', actions: [{ tool: 'Read', input: 'a', result: 'r1' }] },
|
||||
{ turn: 4, user: 'Stop hook feedback: x', assistant: 'второй ответ', actions: [{ tool: 'Grep', input: 'b', result: 'r2' }] },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user