fix context compressor summary prompt (#1041)
This commit is contained in:
@@ -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<string, unknown>): Promise<void> {
|
||||
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<typeof getEncoding> | 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<AgentBridgeRunResult>({
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user