Skip to content

Commit e57a3a8

Browse files
committed
Refactor: Unify pruning output formatting
- Centralize formatting logic in display-utils.ts to share between tool output and notifications - Update pruning tool to return a detailed, human-readable summary of actions taken - Simplify notification logic by reusing shared formatting functions
1 parent 4a749e3 commit e57a3a8

File tree

3 files changed

+141
-119
lines changed

3 files changed

+141
-119
lines changed

lib/pruning-tool.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import { resetToolTrackerCount } from "./fetch-wrapper/tool-tracker"
77
import { isSubagentSession, findCurrentAgent } from "./hooks"
88
import { getActualId } from "./state/id-mapping"
99
import { sendUnifiedNotification, type NotificationContext } from "./ui/notification"
10+
import { formatPruningResultForTool } from "./ui/display-utils"
1011
import { ensureSessionRestored } from "./state"
1112
import { saveSessionState } from "./state/persistence"
1213
import type { Logger } from "./logger"
1314
import { estimateTokensBatch } from "./tokenizer"
14-
import type { SessionStats } from "./core/janitor"
15+
import type { SessionStats, PruningResult } from "./core/janitor"
1516
import { loadPrompt } from "./core/prompt"
1617

1718
/** Tool description loaded from prompts/tool.txt */
@@ -122,8 +123,15 @@ export function createPruningTool(
122123
resetToolTrackerCount(toolTracker)
123124
}
124125

125-
// Return empty string on success (like edit tool) - guidance is in tool description
126-
return ""
126+
const result: PruningResult = {
127+
prunedCount: prunedIds.length,
128+
tokensSaved,
129+
llmPrunedIds: prunedIds,
130+
toolMetadata,
131+
sessionStats
132+
}
133+
134+
return formatPruningResultForTool(result, ctx.workingDirectory)
127135
},
128136
})
129137
}

lib/ui/display-utils.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import type { ToolMetadata } from "../fetch-wrapper/types"
2+
import type { PruningResult } from "../core/janitor"
3+
14
/**
25
* Extracts a human-readable key from tool metadata for display purposes.
36
* Used by both deduplication and AI analysis to show what was pruned.
@@ -71,3 +74,90 @@ export function extractParameterKey(metadata: { tool: string, parameters?: any }
7174
}
7275
return paramStr.substring(0, 50)
7376
}
77+
78+
export function truncate(str: string, maxLen: number = 60): string {
79+
if (str.length <= maxLen) return str
80+
return str.slice(0, maxLen - 3) + '...'
81+
}
82+
83+
export function shortenPath(input: string, workingDirectory?: string): string {
84+
const inPathMatch = input.match(/^(.+) in (.+)$/)
85+
if (inPathMatch) {
86+
const prefix = inPathMatch[1]
87+
const pathPart = inPathMatch[2]
88+
const shortenedPath = shortenSinglePath(pathPart, workingDirectory)
89+
return `${prefix} in ${shortenedPath}`
90+
}
91+
92+
return shortenSinglePath(input, workingDirectory)
93+
}
94+
95+
function shortenSinglePath(path: string, workingDirectory?: string): string {
96+
if (workingDirectory) {
97+
if (path.startsWith(workingDirectory + '/')) {
98+
return path.slice(workingDirectory.length + 1)
99+
}
100+
if (path === workingDirectory) {
101+
return '.'
102+
}
103+
}
104+
105+
return path
106+
}
107+
108+
/**
109+
* Formats a list of pruned items in the style: "→ tool: parameter"
110+
*/
111+
export function formatPrunedItemsList(
112+
prunedIds: string[],
113+
toolMetadata: Map<string, ToolMetadata>,
114+
workingDirectory?: string
115+
): string[] {
116+
const lines: string[] = []
117+
118+
for (const prunedId of prunedIds) {
119+
const normalizedId = prunedId.toLowerCase()
120+
const metadata = toolMetadata.get(normalizedId)
121+
122+
if (metadata) {
123+
const paramKey = extractParameterKey(metadata)
124+
if (paramKey) {
125+
// Use 60 char limit to match notification style
126+
const displayKey = truncate(shortenPath(paramKey, workingDirectory), 60)
127+
lines.push(`→ ${metadata.tool}: ${displayKey}`)
128+
} else {
129+
lines.push(`→ ${metadata.tool}`)
130+
}
131+
}
132+
}
133+
134+
const knownCount = prunedIds.filter(id =>
135+
toolMetadata.has(id.toLowerCase())
136+
).length
137+
const unknownCount = prunedIds.length - knownCount
138+
139+
if (unknownCount > 0) {
140+
lines.push(`→ (${unknownCount} tool${unknownCount > 1 ? 's' : ''} with unknown metadata)`)
141+
}
142+
143+
return lines
144+
}
145+
146+
/**
147+
* Formats a PruningResult into a human-readable string for the prune tool output.
148+
*/
149+
export function formatPruningResultForTool(
150+
result: PruningResult,
151+
workingDirectory?: string
152+
): string {
153+
const lines: string[] = []
154+
lines.push(`Context pruning complete. Pruned ${result.prunedCount} tool outputs.`)
155+
lines.push('')
156+
157+
if (result.llmPrunedIds.length > 0) {
158+
lines.push(`Semantically pruned (${result.llmPrunedIds.length}):`)
159+
lines.push(...formatPrunedItemsList(result.llmPrunedIds, result.toolMetadata, workingDirectory))
160+
}
161+
162+
return lines.join('\n').trim()
163+
}

lib/ui/notification.ts

Lines changed: 40 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { Logger } from "../logger"
22
import type { SessionStats, GCStats } from "../core/janitor"
33
import type { ToolMetadata } from "../fetch-wrapper/types"
44
import { formatTokenCount } from "../tokenizer"
5-
import { extractParameterKey } from "./display-utils"
5+
import { formatPrunedItemsList } from "./display-utils"
66

77
export type PruningSummaryLevel = "off" | "minimal" | "detailed"
88

@@ -26,6 +26,31 @@ export interface NotificationData {
2626
sessionStats: SessionStats | null
2727
}
2828

29+
export async function sendUnifiedNotification(
30+
ctx: NotificationContext,
31+
sessionID: string,
32+
data: NotificationData,
33+
agent?: string
34+
): Promise<boolean> {
35+
const hasAiPruning = data.aiPrunedCount > 0
36+
const hasGcActivity = data.gcPending && data.gcPending.toolsDeduped > 0
37+
38+
if (!hasAiPruning && !hasGcActivity) {
39+
return false
40+
}
41+
42+
if (ctx.config.pruningSummary === 'off') {
43+
return false
44+
}
45+
46+
const message = ctx.config.pruningSummary === 'minimal'
47+
? buildMinimalMessage(data)
48+
: buildDetailedMessage(data, ctx.config.workingDirectory)
49+
50+
await sendIgnoredMessage(ctx, sessionID, message, agent)
51+
return true
52+
}
53+
2954
export async function sendIgnoredMessage(
3055
ctx: NotificationContext,
3156
sessionID: string,
@@ -50,35 +75,25 @@ export async function sendIgnoredMessage(
5075
}
5176
}
5277

53-
export async function sendUnifiedNotification(
54-
ctx: NotificationContext,
55-
sessionID: string,
56-
data: NotificationData,
57-
agent?: string
58-
): Promise<boolean> {
59-
const hasAiPruning = data.aiPrunedCount > 0
60-
const hasGcActivity = data.gcPending && data.gcPending.toolsDeduped > 0
61-
62-
if (!hasAiPruning && !hasGcActivity) {
63-
return false
64-
}
78+
function buildMinimalMessage(data: NotificationData): string {
79+
const { justNowTokens, totalTokens } = calculateStats(data)
80+
return formatStatsHeader(totalTokens, justNowTokens)
81+
}
6582

66-
if (ctx.config.pruningSummary === 'off') {
67-
return false
68-
}
83+
function buildDetailedMessage(data: NotificationData, workingDirectory?: string): string {
84+
const { justNowTokens, totalTokens } = calculateStats(data)
6985

70-
const message = ctx.config.pruningSummary === 'minimal'
71-
? buildMinimalMessage(data)
72-
: buildDetailedMessage(data, ctx.config.workingDirectory)
86+
let message = formatStatsHeader(totalTokens, justNowTokens)
7387

74-
await sendIgnoredMessage(ctx, sessionID, message, agent)
75-
return true
76-
}
88+
if (data.aiPrunedCount > 0) {
89+
const justNowTokensStr = `~${formatTokenCount(justNowTokens)}`
90+
message += `\n\n▣ Pruned tools (${justNowTokensStr})`
7791

78-
function buildMinimalMessage(data: NotificationData): string {
79-
const { justNowTokens, totalTokens } = calculateStats(data)
92+
const itemLines = formatPrunedItemsList(data.aiPrunedIds, data.toolMetadata, workingDirectory)
93+
message += '\n' + itemLines.join('\n')
94+
}
8095

81-
return formatStatsHeader(totalTokens, justNowTokens)
96+
return message.trim()
8297
}
8398

8499
function calculateStats(data: NotificationData): {
@@ -108,94 +123,3 @@ function formatStatsHeader(
108123
`▣ DCP | ${totalTokensPadded} saved total`,
109124
].join('\n')
110125
}
111-
112-
function buildDetailedMessage(data: NotificationData, workingDirectory?: string): string {
113-
const { justNowTokens, totalTokens } = calculateStats(data)
114-
115-
let message = formatStatsHeader(totalTokens, justNowTokens)
116-
117-
if (data.aiPrunedCount > 0) {
118-
const justNowTokensStr = `~${formatTokenCount(justNowTokens)}`
119-
message += `\n\n▣ Pruned tools (${justNowTokensStr})`
120-
121-
for (const prunedId of data.aiPrunedIds) {
122-
const normalizedId = prunedId.toLowerCase()
123-
const metadata = data.toolMetadata.get(normalizedId)
124-
125-
if (metadata) {
126-
const paramKey = extractParameterKey(metadata)
127-
if (paramKey) {
128-
const displayKey = truncate(shortenPath(paramKey, workingDirectory), 60)
129-
message += `\n→ ${metadata.tool}: ${displayKey}`
130-
} else {
131-
message += `\n→ ${metadata.tool}`
132-
}
133-
}
134-
}
135-
136-
const knownCount = data.aiPrunedIds.filter(id =>
137-
data.toolMetadata.has(id.toLowerCase())
138-
).length
139-
const unknownCount = data.aiPrunedIds.length - knownCount
140-
141-
if (unknownCount > 0) {
142-
message += `\n→ (${unknownCount} tool${unknownCount > 1 ? 's' : ''} with unknown metadata)`
143-
}
144-
}
145-
146-
return message.trim()
147-
}
148-
149-
function truncate(str: string, maxLen: number = 60): string {
150-
if (str.length <= maxLen) return str
151-
return str.slice(0, maxLen - 3) + '...'
152-
}
153-
154-
function shortenPath(input: string, workingDirectory?: string): string {
155-
const inPathMatch = input.match(/^(.+) in (.+)$/)
156-
if (inPathMatch) {
157-
const prefix = inPathMatch[1]
158-
const pathPart = inPathMatch[2]
159-
const shortenedPath = shortenSinglePath(pathPart, workingDirectory)
160-
return `${prefix} in ${shortenedPath}`
161-
}
162-
163-
return shortenSinglePath(input, workingDirectory)
164-
}
165-
166-
function shortenSinglePath(path: string, workingDirectory?: string): string {
167-
const homeDir = require('os').homedir()
168-
169-
if (workingDirectory) {
170-
if (path.startsWith(workingDirectory + '/')) {
171-
return path.slice(workingDirectory.length + 1)
172-
}
173-
if (path === workingDirectory) {
174-
return '.'
175-
}
176-
}
177-
178-
if (path.startsWith(homeDir)) {
179-
path = '~' + path.slice(homeDir.length)
180-
}
181-
182-
const nodeModulesMatch = path.match(/node_modules\/(@[^\/]+\/[^\/]+|[^\/]+)\/(.*)/)
183-
if (nodeModulesMatch) {
184-
return `${nodeModulesMatch[1]}/${nodeModulesMatch[2]}`
185-
}
186-
187-
if (workingDirectory) {
188-
const workingDirWithTilde = workingDirectory.startsWith(homeDir)
189-
? '~' + workingDirectory.slice(homeDir.length)
190-
: null
191-
192-
if (workingDirWithTilde && path.startsWith(workingDirWithTilde + '/')) {
193-
return path.slice(workingDirWithTilde.length + 1)
194-
}
195-
if (workingDirWithTilde && path === workingDirWithTilde) {
196-
return '.'
197-
}
198-
}
199-
200-
return path
201-
}

0 commit comments

Comments
 (0)