diff --git a/tools/secretary-harvest.mjs b/tools/secretary-harvest.mjs index dee2fec..c48c6f1 100644 --- a/tools/secretary-harvest.mjs +++ b/tools/secretary-harvest.mjs @@ -119,10 +119,17 @@ function clamp(s) { return s.slice(0, cap) + `…[вырезано ${s.length - cap} знаков]`; } -/** Текст обмена из спана {user, assistant, actions} (заменяет exchange(span) песочницы). */ +// Сколько ПОСЛЕДНИХ действий хода показывать ролям/редактору: длинный ход с десятками Read +// не возим целиком (анти-раздувание). Суть хода всё равно фиксирует «step» редактора. +export const MAX_EXCHANGE_ACTIONS = 7; + +/** Текст обмена из спана {user, assistant, actions}. Показываем последние MAX_EXCHANGE_ACTIONS действий. */ export function renderExchangeText(spanEx) { - const acts = ((spanEx.actions || []).map((a) => ` • ${a.tool} in=${a.input ?? ''}${a.result != null ? ` → ${clamp(String(a.result))}` : ''}`).join('\n')) || '—'; - return `[ЮЗЕР]: ${spanEx.user || ''}\n[АССИСТЕНТ]: ${spanEx.assistant || ''}\n[ДЕЙСТВИЯ]:\n${acts}`; + const all = spanEx.actions || []; + const shown = all.slice(-MAX_EXCHANGE_ACTIONS); + const head = all.length > shown.length ? ` (показаны последние ${shown.length} из ${all.length} действий)\n` : ''; + const acts = (shown.map((a) => ` • ${a.tool} in=${a.input ?? ''}${a.result != null ? ` → ${clamp(String(a.result))}` : ''}`).join('\n')) || '—'; + return `[ЮЗЕР]: ${spanEx.user || ''}\n[АССИСТЕНТ]: ${spanEx.assistant || ''}\n[ДЕЙСТВИЯ]:\n${head}${acts}`; } // ── USER-сборщики (берут готовый spanText) ──────────────────────────────── diff --git a/tools/secretary-harvest.test.mjs b/tools/secretary-harvest.test.mjs index 2bb4b15..ec6437d 100644 --- a/tools/secretary-harvest.test.mjs +++ b/tools/secretary-harvest.test.mjs @@ -134,3 +134,20 @@ describe('renderExchangeText input cap', () => { delete process.env.SECRETARY_INPUT_CAP; }); }); + +describe('renderExchangeText — последние 7 действий', () => { + it('режет до последних 7 и помечает сколько было', () => { + const actions = Array.from({ length: 10 }, (_, i) => ({ tool: 'Read', input: `f${i}`, result: `r${i}` })); + const t = renderExchangeText({ user: 'u', assistant: 'a', actions }); + expect(t).toContain('f9'); // последнее показано + expect(t).toContain('f3'); // граница последних 7 (индексы 3..9) + expect(t).not.toContain('f2'); // 8-е с конца отброшено + expect(t).toMatch(/последние 7 из 10/); + }); + it('≤7 действий — без пометки', () => { + const actions = Array.from({ length: 5 }, (_, i) => ({ tool: 'Read', input: `f${i}`, result: `r${i}` })); + const t = renderExchangeText({ user: 'u', assistant: 'a', actions }); + expect(t).toContain('f0'); + expect(t).not.toMatch(/последние \d+ из/); + }); +}); diff --git a/tools/secretary-reconcile.mjs b/tools/secretary-reconcile.mjs index 703131e..180abb6 100644 --- a/tools/secretary-reconcile.mjs +++ b/tools/secretary-reconcile.mjs @@ -1,5 +1,9 @@ // Секретарь-«редактор»: модель правит весь протокол, хук сторожит потери (спека reconcile). +// Сколько ПОСЛЕДНИХ действий хода подавать редактору (анти-раздувание: длинный ход с десятками +// Read не возим целиком). Держим в синхроне с MAX_EXCHANGE_ACTIONS из secretary-harvest.mjs. +const MAX_EXCHANGE_ACTIONS = 7; + /** Запрос к модели-редактору: весь протокол + обмен → весь обновлённый протокол. */ export function buildReconcilePrompt({ protocol = {}, lastExchange = {}, remark = null } = {}) { const system = [ @@ -27,8 +31,11 @@ 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 acts = ((lastExchange.actions || []).map((a) => - ` • ${a.tool} in=${a.input ?? ''}${a.result != null ? `\n → ${String(a.result).replace(/\n/g, '\n ')}` : ''}`).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 user = [ `Тема дела: ${protocol.subject || '(нет)'}`, sec('Решения', protocol.decisions), sec('Альтернативы', protocol.alternatives), diff --git a/tools/secretary-reconcile.test.mjs b/tools/secretary-reconcile.test.mjs index 79e6284..e2a7a96 100644 --- a/tools/secretary-reconcile.test.mjs +++ b/tools/secretary-reconcile.test.mjs @@ -124,6 +124,14 @@ 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/); + }); }); describe('reconcile — 9 категорий + стабильная тема', () => {