From b0000b4c38ab7771986c83dbeb60c33a4547c8fb Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Tue, 26 May 2026 17:29:19 +0800 Subject: [PATCH] fix context compressor summary prompt (#1041) --- .../src/lib/context-compressor/index.ts | 47 ++++++++++++++++--- .../services/hermes/run-chat/compression.ts | 1 - tests/server/context-compressor.test.ts | 16 +++++++ 3 files changed, 57 insertions(+), 7 deletions(-) diff --git a/packages/server/src/lib/context-compressor/index.ts b/packages/server/src/lib/context-compressor/index.ts index 4f4caa3..cb29b54 100644 --- a/packages/server/src/lib/context-compressor/index.ts +++ b/packages/server/src/lib/context-compressor/index.ts @@ -15,6 +15,8 @@ import { encodingForModel, getEncoding } from 'js-tiktoken' import { randomUUID } from 'crypto' +import { mkdir, writeFile } from 'fs/promises' +import { resolve } from 'path' import { logger } from '../../services/logger' import { AgentBridgeClient, type AgentBridgeRunResult } from '../../services/hermes/agent-bridge' import { @@ -82,6 +84,25 @@ export interface SummarizerOptions { workerKey?: string } +const SUMMARIZER_TRIGGER_MESSAGE = 'Generate the context checkpoint summary now.' +const SUMMARIZER_DEBUG_DIR = 'logs/context-compressor' +const SUMMARIZER_DEBUG_FILE = 'summarizer-debug.json' + +async function writeSummarizerDebugDump(payload: Record): Promise { + if (process.env.NODE_ENV !== 'development') return + try { + const debugDir = resolve(process.cwd(), SUMMARIZER_DEBUG_DIR) + await mkdir(debugDir, { recursive: true }) + await writeFile( + resolve(debugDir, SUMMARIZER_DEBUG_FILE), + `${JSON.stringify(payload, null, 2)}\n`, + 'utf8', + ) + } catch (err) { + logger.warn(err, '[context-compressor] failed to write summarizer debug dump') + } +} + // ─── Token counting ───────────────────────────────────── let _encoder: ReturnType | null = null @@ -444,24 +465,40 @@ export async function callSummarizer( ? { profile: summarizer } : summarizer || {} const profile = options.profile || 'default' - const convHistory: Array<{ role: string; content: string }> = [...history] + void history + const convHistory: Array<{ role: string; content: string }> = [] if (previousSummary) { convHistory.unshift( { role: 'user', content: `[Previous summary]\n${previousSummary}` }, { role: 'assistant', content: 'Understood, I will update the summary.' }, + { role: 'user', content: prompt }, ) + } else { + convHistory.unshift({ role: 'user', content: prompt }) } const bridge = new AgentBridgeClient({ timeoutMs: timeoutMs + 15_000 }) const sessionId = `compress_${Date.now().toString(36)}_${randomUUID().replace(/-/g, '').slice(0, 12)}` const workerKey = options.workerKey || `${profile}:compression:${sessionId}` + const message = SUMMARIZER_TRIGGER_MESSAGE + + await writeSummarizerDebugDump({ + writtenAt: new Date().toISOString(), + sessionId, + workerKey, + profile, + model: options.model || null, + provider: options.provider || null, + message, + convHistory, + }) try { const result = await bridge.request({ action: 'chat', session_id: sessionId, - message: prompt, + message, conversation_history: convHistory, profile, worker_key: workerKey, @@ -622,10 +659,9 @@ export class ChatContextCompressor { try { const contentToSummarize = serializeForSummary(toCompress) const prompt = buildIncrementalPrompt(previousSummary, contentToSummarize, this.config.summaryBudget) - const history = buildConversationHistory(toCompress) const t0 = Date.now() - summary = await callSummarizer(upstream, apiKey, prompt, history, this.config.summarizationTimeoutMs, previousSummary, summarizer) + summary = await callSummarizer(upstream, apiKey, prompt, [], this.config.summarizationTimeoutMs, previousSummary, summarizer) logger.info('[context-compressor] incremental-llm done in %dms, %d chars', Date.now() - t0, summary.length) } catch (err: any) { logger.warn('[context-compressor] incremental-llm failed: %s — keeping new messages verbatim', err.message) @@ -701,12 +737,11 @@ export class ChatContextCompressor { const contentToSummarize = serializeForSummary(toCompress) const prompt = buildFullPrompt(contentToSummarize, this.config.summaryBudget) - const history = buildConversationHistory(toCompress) let summary: string | null = null try { const t0 = Date.now() - summary = await callSummarizer(upstream, apiKey, prompt, history, this.config.summarizationTimeoutMs, undefined, summarizer) + summary = await callSummarizer(upstream, apiKey, prompt, [], this.config.summarizationTimeoutMs, undefined, summarizer) logger.info('[context-compressor] full-llm done in %dms, %d chars', Date.now() - t0, summary.length) } catch (err: any) { logger.warn('[context-compressor] full-llm failed: %s', err.message) diff --git a/packages/server/src/services/hermes/run-chat/compression.ts b/packages/server/src/services/hermes/run-chat/compression.ts index 89dc625..29e383d 100644 --- a/packages/server/src/services/hermes/run-chat/compression.ts +++ b/packages/server/src/services/hermes/run-chat/compression.ts @@ -333,7 +333,6 @@ export async function buildCompressedHistory( history = await compressHistory(history, null, sessionId, upstream, apiKey, cState, totalTokens, emit, sessionMap, modelContext, compressionConfig.compressor, currentRunInputTokens) } } - return history } catch (err) { if (isContextWindowTooSmallError(err)) throw err diff --git a/tests/server/context-compressor.test.ts b/tests/server/context-compressor.test.ts index 4915cf6..ba7bfba 100644 --- a/tests/server/context-compressor.test.ts +++ b/tests/server/context-compressor.test.ts @@ -178,8 +178,14 @@ describe('ChatContextCompressor', () => { action: 'chat', profile: 'default', worker_key: 'default:compression:s1', + message: 'Generate the context checkpoint summary now.', wait: true, }), expect.any(Object)) + const request = bridgeRequestMock.mock.calls[0][0] + expect(request.conversation_history[0]).toEqual(expect.objectContaining({ + role: 'user', + content: expect.stringContaining('TURNS TO SUMMARIZE:'), + })) const compressSessionId = bridgeRequestMock.mock.calls[0][0].session_id expect(String(compressSessionId)).toMatch(/^compress_/) expect(bridgeDestroyMock).toHaveBeenCalledWith( @@ -471,6 +477,16 @@ describe('ChatContextCompressor', () => { const result = await compressor.compress(messages, 'http://upstream', undefined, 's1') expect(bridgeRequestMock).toHaveBeenCalledTimes(1) + const request = bridgeRequestMock.mock.calls[0][0] + expect(request.message).toBe('Generate the context checkpoint summary now.') + expect(request.conversation_history.slice(0, 3)).toEqual([ + { role: 'user', content: '[Previous summary]\nprevious summary' }, + { role: 'assistant', content: 'Understood, I will update the summary.' }, + expect.objectContaining({ + role: 'user', + content: expect.stringContaining('NEW TURNS TO INCORPORATE:'), + }), + ]) expect(result.messages.map(m => m.content)).toEqual([ 'head 0', 'head 1',