Skip to content

Commit 6ab7f24

Browse files
committed
feat(ui): render positional compress graph
1 parent e2cb885 commit 6ab7f24

2 files changed

Lines changed: 129 additions & 30 deletions

File tree

lib/ui/notification.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ export async function sendCompressNotification(
174174
})
175175

176176
message += `\n\n${progressBar}`
177-
message += `\n▣ Compressing (${pruneTokenCounterStr} removed, ${reduction}% reduction)`
177+
message += `\n\n▣ Compressing (${pruneTokenCounterStr} removed, ${reduction}% reduction)`
178178
message += `\n→ Topic: ${topic}`
179179
message += `\n→ Items: ${messageIds.length} messages`
180180
if (toolIds.length > 0) {

lib/ui/utils.ts

Lines changed: 128 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,39 @@ export interface CompressionGraphData {
161161
olderCompressedTokens: number
162162
remainingTokens: number
163163
totalSessionTokens: number
164+
segments: CompressionGraphSegment[]
165+
}
166+
167+
type CompressionGraphSegmentType = "system" | "recentCompressed" | "olderCompressed" | "inContext"
168+
169+
export interface CompressionGraphSegment {
170+
type: CompressionGraphSegmentType
171+
tokens: number
172+
}
173+
174+
function appendGraphSegment(
175+
segments: CompressionGraphSegment[],
176+
type: CompressionGraphSegmentType,
177+
tokens: number,
178+
): void {
179+
if (tokens <= 0) {
180+
return
181+
}
182+
183+
const last = segments[segments.length - 1]
184+
if (last && last.type === type) {
185+
last.tokens += tokens
186+
return
187+
}
188+
189+
segments.push({ type, tokens })
190+
}
191+
192+
function incrementMapValue(map: Map<string, number>, key: string, value: number): void {
193+
if (value <= 0) {
194+
return
195+
}
196+
map.set(key, (map.get(key) || 0) + value)
164197
}
165198

166199
function countMessageTokensExcludingPrunedTools(state: SessionState, msg: WithParts): number {
@@ -256,19 +289,44 @@ export function buildCompressionGraphData(
256289
): CompressionGraphData {
257290
const toolParentMap = buildToolParentMap(messages)
258291
const prunedMessageIds = new Set(state.prune.messages.keys())
292+
const messageIds = new Set(messages.map((m) => m.info.id))
259293

260294
let compressedMessageTokens = 0
261295
for (const tokens of state.prune.messages.values()) {
262296
compressedMessageTokens += tokens
263297
}
264298

299+
const recentStandaloneByMessage = new Map<string, number>()
300+
const olderStandaloneByMessage = new Map<string, number>()
301+
302+
let unparentedRecentStandaloneTokens = 0
303+
let unparentedOlderStandaloneTokens = 0
265304
let compressedStandaloneToolTokens = 0
305+
let recentStandaloneToolTokens = 0
266306
for (const [toolId, toolTokens] of state.prune.tools.entries()) {
267307
const parentMessageId = toolParentMap.get(toolId)
268308
if (parentMessageId && prunedMessageIds.has(parentMessageId)) {
269309
continue
270310
}
311+
271312
compressedStandaloneToolTokens += toolTokens
313+
314+
const isRecent = newToolIds.has(toolId)
315+
if (isRecent) {
316+
recentStandaloneToolTokens += toolTokens
317+
}
318+
319+
if (parentMessageId) {
320+
incrementMapValue(
321+
isRecent ? recentStandaloneByMessage : olderStandaloneByMessage,
322+
parentMessageId,
323+
toolTokens,
324+
)
325+
} else if (isRecent) {
326+
unparentedRecentStandaloneTokens += toolTokens
327+
} else {
328+
unparentedOlderStandaloneTokens += toolTokens
329+
}
272330
}
273331

274332
const compressedTotalTokens = compressedMessageTokens + compressedStandaloneToolTokens
@@ -278,25 +336,21 @@ export function buildCompressionGraphData(
278336
recentMessageTokens += state.prune.messages.get(messageId) || 0
279337
}
280338

281-
let recentStandaloneToolTokens = 0
282-
for (const toolId of newToolIds) {
283-
const parentMessageId = toolParentMap.get(toolId)
284-
285-
if (parentMessageId && newMessageIds.has(parentMessageId)) {
286-
continue
287-
}
339+
const recentCompressedTokens = recentMessageTokens + recentStandaloneToolTokens
340+
const olderCompressedTokens = Math.max(0, compressedTotalTokens - recentCompressedTokens)
288341

289-
if (parentMessageId && prunedMessageIds.has(parentMessageId)) {
342+
const summaryTokensByAnchor = new Map<string, number>()
343+
let summaryTokensTotal = 0
344+
for (const summary of state.compressSummaries) {
345+
if (!messageIds.has(summary.anchorMessageId)) {
290346
continue
291347
}
292348

293-
recentStandaloneToolTokens += state.prune.tools.get(toolId) || 0
349+
const tokens = countTokens(summary.summary)
350+
summaryTokensTotal += tokens
351+
incrementMapValue(summaryTokensByAnchor, summary.anchorMessageId, tokens)
294352
}
295353

296-
const recentCompressedTokens = recentMessageTokens + recentStandaloneToolTokens
297-
const olderCompressedTokens = Math.max(0, compressedTotalTokens - recentCompressedTokens)
298-
299-
const messageIds = new Set(messages.map((m) => m.info.id))
300354
let remainingTokens = 0
301355

302356
for (const msg of messages) {
@@ -309,23 +363,54 @@ export function buildCompressionGraphData(
309363
remainingTokens += countMessageTokensExcludingPrunedTools(state, msg)
310364
}
311365

312-
for (const summary of state.compressSummaries) {
313-
if (!messageIds.has(summary.anchorMessageId)) {
314-
continue
315-
}
316-
remainingTokens += countTokens(summary.summary)
317-
}
366+
remainingTokens += summaryTokensTotal
318367

319368
const systemTokens = state.systemPromptTokens ?? 0
320369
const totalSessionTokens =
321370
systemTokens + recentCompressedTokens + olderCompressedTokens + remainingTokens
322371

372+
const segments: CompressionGraphSegment[] = []
373+
appendGraphSegment(segments, "system", systemTokens)
374+
375+
for (const msg of messages) {
376+
const messageId = msg.info.id
377+
const summaryTokens = summaryTokensByAnchor.get(messageId) || 0
378+
appendGraphSegment(segments, "inContext", summaryTokens)
379+
380+
if (prunedMessageIds.has(messageId)) {
381+
const messageTokens = state.prune.messages.get(messageId) || 0
382+
appendGraphSegment(
383+
segments,
384+
newMessageIds.has(messageId) ? "recentCompressed" : "olderCompressed",
385+
messageTokens,
386+
)
387+
} else if (!(msg.info.role === "user" && isIgnoredUserMessage(msg))) {
388+
const messageTokens = countMessageTokensExcludingPrunedTools(state, msg)
389+
appendGraphSegment(segments, "inContext", messageTokens)
390+
}
391+
392+
appendGraphSegment(
393+
segments,
394+
"recentCompressed",
395+
recentStandaloneByMessage.get(messageId) || 0,
396+
)
397+
appendGraphSegment(
398+
segments,
399+
"olderCompressed",
400+
olderStandaloneByMessage.get(messageId) || 0,
401+
)
402+
}
403+
404+
appendGraphSegment(segments, "recentCompressed", unparentedRecentStandaloneTokens)
405+
appendGraphSegment(segments, "olderCompressed", unparentedOlderStandaloneTokens)
406+
323407
clog.info(C.COMPRESS, "Compression graph token accounting", {
324408
systemTokens,
325409
recentCompressedTokens,
326410
olderCompressedTokens,
327411
remainingTokens,
328412
totalSessionTokens,
413+
segments: segments.length,
329414
})
330415

331416
return {
@@ -334,6 +419,7 @@ export function buildCompressionGraphData(
334419
olderCompressedTokens,
335420
remainingTokens,
336421
totalSessionTokens,
422+
segments,
337423
}
338424
}
339425

@@ -359,18 +445,31 @@ function allocateSegmentWidths(values: number[], total: number, width: number):
359445
}
360446

361447
export function formatCompressionGraph(data: CompressionGraphData, width: number = 50): string {
362-
const values = [
363-
data.systemTokens,
364-
data.recentCompressedTokens,
365-
data.olderCompressedTokens,
366-
data.remainingTokens,
367-
]
368-
const chars = ["▌", "⣿", "░", "█"]
369-
const segmentWidths = allocateSegmentWidths(values, data.totalSessionTokens, width)
448+
const segments: CompressionGraphSegment[] =
449+
data.segments.length > 0
450+
? data.segments
451+
: [
452+
{ type: "system", tokens: data.systemTokens },
453+
{ type: "recentCompressed", tokens: data.recentCompressedTokens },
454+
{ type: "olderCompressed", tokens: data.olderCompressedTokens },
455+
{ type: "inContext", tokens: data.remainingTokens },
456+
]
457+
458+
const chars: Record<CompressionGraphSegmentType, string> = {
459+
system: "▌",
460+
recentCompressed: "⣿",
461+
olderCompressed: "░",
462+
inContext: "█",
463+
}
464+
const segmentWidths = allocateSegmentWidths(
465+
segments.map((segment) => segment.tokens),
466+
data.totalSessionTokens,
467+
width,
468+
)
370469

371470
let bar = ""
372-
for (let i = 0; i < segmentWidths.length; i++) {
373-
bar += chars[i].repeat(Math.max(0, segmentWidths[i]))
471+
for (let i = 0; i < segments.length; i++) {
472+
bar += chars[segments[i].type].repeat(Math.max(0, segmentWidths[i]))
374473
}
375474

376475
if (bar.length < width) {

0 commit comments

Comments
 (0)