fix context compressor summary prompt (#1041)
This commit is contained in:
@@ -15,6 +15,8 @@
|
|||||||
|
|
||||||
import { encodingForModel, getEncoding } from 'js-tiktoken'
|
import { encodingForModel, getEncoding } from 'js-tiktoken'
|
||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
|
import { mkdir, writeFile } from 'fs/promises'
|
||||||
|
import { resolve } from 'path'
|
||||||
import { logger } from '../../services/logger'
|
import { logger } from '../../services/logger'
|
||||||
import { AgentBridgeClient, type AgentBridgeRunResult } from '../../services/hermes/agent-bridge'
|
import { AgentBridgeClient, type AgentBridgeRunResult } from '../../services/hermes/agent-bridge'
|
||||||
import {
|
import {
|
||||||
@@ -82,6 +84,25 @@ export interface SummarizerOptions {
|
|||||||
workerKey?: string
|
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 ─────────────────────────────────────
|
// ─── Token counting ─────────────────────────────────────
|
||||||
|
|
||||||
let _encoder: ReturnType<typeof getEncoding> | null = null
|
let _encoder: ReturnType<typeof getEncoding> | null = null
|
||||||
@@ -444,24 +465,40 @@ export async function callSummarizer(
|
|||||||
? { profile: summarizer }
|
? { profile: summarizer }
|
||||||
: summarizer || {}
|
: summarizer || {}
|
||||||
const profile = options.profile || 'default'
|
const profile = options.profile || 'default'
|
||||||
const convHistory: Array<{ role: string; content: string }> = [...history]
|
void history
|
||||||
|
const convHistory: Array<{ role: string; content: string }> = []
|
||||||
|
|
||||||
if (previousSummary) {
|
if (previousSummary) {
|
||||||
convHistory.unshift(
|
convHistory.unshift(
|
||||||
{ role: 'user', content: `[Previous summary]\n${previousSummary}` },
|
{ role: 'user', content: `[Previous summary]\n${previousSummary}` },
|
||||||
{ role: 'assistant', content: 'Understood, I will update the summary.' },
|
{ 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 bridge = new AgentBridgeClient({ timeoutMs: timeoutMs + 15_000 })
|
||||||
const sessionId = `compress_${Date.now().toString(36)}_${randomUUID().replace(/-/g, '').slice(0, 12)}`
|
const sessionId = `compress_${Date.now().toString(36)}_${randomUUID().replace(/-/g, '').slice(0, 12)}`
|
||||||
const workerKey = options.workerKey || `${profile}:compression:${sessionId}`
|
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 {
|
try {
|
||||||
const result = await bridge.request<AgentBridgeRunResult>({
|
const result = await bridge.request<AgentBridgeRunResult>({
|
||||||
action: 'chat',
|
action: 'chat',
|
||||||
session_id: sessionId,
|
session_id: sessionId,
|
||||||
message: prompt,
|
message,
|
||||||
conversation_history: convHistory,
|
conversation_history: convHistory,
|
||||||
profile,
|
profile,
|
||||||
worker_key: workerKey,
|
worker_key: workerKey,
|
||||||
@@ -622,10 +659,9 @@ export class ChatContextCompressor {
|
|||||||
try {
|
try {
|
||||||
const contentToSummarize = serializeForSummary(toCompress)
|
const contentToSummarize = serializeForSummary(toCompress)
|
||||||
const prompt = buildIncrementalPrompt(previousSummary, contentToSummarize, this.config.summaryBudget)
|
const prompt = buildIncrementalPrompt(previousSummary, contentToSummarize, this.config.summaryBudget)
|
||||||
const history = buildConversationHistory(toCompress)
|
|
||||||
|
|
||||||
const t0 = Date.now()
|
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)
|
logger.info('[context-compressor] incremental-llm done in %dms, %d chars', Date.now() - t0, summary.length)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.warn('[context-compressor] incremental-llm failed: %s — keeping new messages verbatim', err.message)
|
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 contentToSummarize = serializeForSummary(toCompress)
|
||||||
const prompt = buildFullPrompt(contentToSummarize, this.config.summaryBudget)
|
const prompt = buildFullPrompt(contentToSummarize, this.config.summaryBudget)
|
||||||
const history = buildConversationHistory(toCompress)
|
|
||||||
|
|
||||||
let summary: string | null = null
|
let summary: string | null = null
|
||||||
try {
|
try {
|
||||||
const t0 = Date.now()
|
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)
|
logger.info('[context-compressor] full-llm done in %dms, %d chars', Date.now() - t0, summary.length)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
logger.warn('[context-compressor] full-llm failed: %s', err.message)
|
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)
|
history = await compressHistory(history, null, sessionId, upstream, apiKey, cState, totalTokens, emit, sessionMap, modelContext, compressionConfig.compressor, currentRunInputTokens)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return history
|
return history
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (isContextWindowTooSmallError(err)) throw err
|
if (isContextWindowTooSmallError(err)) throw err
|
||||||
|
|||||||
@@ -178,8 +178,14 @@ describe('ChatContextCompressor', () => {
|
|||||||
action: 'chat',
|
action: 'chat',
|
||||||
profile: 'default',
|
profile: 'default',
|
||||||
worker_key: 'default:compression:s1',
|
worker_key: 'default:compression:s1',
|
||||||
|
message: 'Generate the context checkpoint summary now.',
|
||||||
wait: true,
|
wait: true,
|
||||||
}), expect.any(Object))
|
}), 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
|
const compressSessionId = bridgeRequestMock.mock.calls[0][0].session_id
|
||||||
expect(String(compressSessionId)).toMatch(/^compress_/)
|
expect(String(compressSessionId)).toMatch(/^compress_/)
|
||||||
expect(bridgeDestroyMock).toHaveBeenCalledWith(
|
expect(bridgeDestroyMock).toHaveBeenCalledWith(
|
||||||
@@ -471,6 +477,16 @@ describe('ChatContextCompressor', () => {
|
|||||||
const result = await compressor.compress(messages, 'http://upstream', undefined, 's1')
|
const result = await compressor.compress(messages, 'http://upstream', undefined, 's1')
|
||||||
|
|
||||||
expect(bridgeRequestMock).toHaveBeenCalledTimes(1)
|
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([
|
expect(result.messages.map(m => m.content)).toEqual([
|
||||||
'head 0',
|
'head 0',
|
||||||
'head 1',
|
'head 1',
|
||||||
|
|||||||
Reference in New Issue
Block a user