Skip to content

Commit e46066a

Browse files
committed
Improve tool output formatting and add usage examples to context_pruning description
- Add detailed usage guidance and examples to context_pruning tool description - Extract shared formatting helpers in janitor (groupDeduplicationDetails, formatDeduplicationLines, formatToolSummaryLines) - Add formatPruningResultForTool method for structured tool output - Remove unused protectedTools parameter from buildAnalysisPrompt - Update janitor to use 'ai-analysis' strategy name
1 parent 1f06014 commit e46066a

File tree

3 files changed

+140
-48
lines changed

3 files changed

+140
-48
lines changed

index.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { tool } from "@opencode-ai/plugin"
44
import { getConfig } from "./lib/config"
55
import { Logger } from "./lib/logger"
66
import { Janitor, type SessionStats } from "./lib/janitor"
7-
import { formatTokenCount } from "./lib/tokenizer"
7+
88
import { checkForUpdates } from "./lib/version-checker"
99

1010
/**
@@ -149,7 +149,7 @@ const plugin: Plugin = (async (ctx) => {
149149
})
150150

151151
// Check for updates on launch (fire and forget)
152-
checkForUpdates(ctx.client, logger).catch(() => {})
152+
checkForUpdates(ctx.client, logger).catch(() => { })
153153

154154
// Show migration toast if config was migrated (delayed to not overlap with version toast)
155155
if (migrations.length > 0) {
@@ -177,7 +177,7 @@ const plugin: Plugin = (async (ctx) => {
177177
if (event.type === "session.status" && event.properties.status.type === "idle") {
178178
// Skip pruning for subagent sessions
179179
if (await isSubagentSession(ctx.client, event.properties.sessionID)) return
180-
180+
181181
// Skip if no idle strategies configured
182182
if (config.strategies.onIdle.length === 0) return
183183

@@ -191,7 +191,7 @@ const plugin: Plugin = (async (ctx) => {
191191
/**
192192
* Chat Params Hook: Caches model info for janitor
193193
*/
194-
"chat.params": async (input, output) => {
194+
"chat.params": async (input, _output) => {
195195
const sessionId = input.sessionID
196196

197197
// Cache model information for this session so janitor can access it
@@ -217,11 +217,37 @@ const plugin: Plugin = (async (ctx) => {
217217
*/
218218
tool: config.strategies.onTool.length > 0 ? {
219219
context_pruning: tool({
220-
description: "Performs semantic pruning on session tool outputs that are no longer " +
221-
"relevant to the current task. Use this to declutter the conversation context and " +
222-
"filter signal from noise when you notice the context is getting cluttered with " +
223-
"outdated information (e.g., after completing a debugging session, switching to a " +
224-
"new task, or when old file reads are no longer needed).",
220+
description: `Performs semantic pruning on session tool outputs that are no longer relevant to the current task. Use this to declutter the conversation context and filter signal from noise when you notice the context is getting cluttered with outdated information.
221+
222+
## When to Use This Tool
223+
224+
- After completing a debugging session or fixing a bug
225+
- When switching focus to a new task or feature
226+
- After exploring multiple files that didn't lead to changes
227+
- When you've been iterating on a difficult problem and some approaches didn't pan out
228+
- When old file reads, greps, or bash outputs are no longer relevant
229+
230+
## Examples
231+
232+
<example>
233+
Working through a list of bugs to fix:
234+
User: Please fix these 5 type errors in the codebase.
235+
Assistant: I'll work through each error. [Fixes first error]
236+
First error fixed. Let me prune the debugging context before moving to the next one.
237+
[Uses context_pruning with reason: "first bug fixed, moving to next task"]
238+
</example>
239+
240+
<example>
241+
After exploring the codebase to understand it:
242+
Assistant: I've reviewed the relevant files. Let me prune the exploratory reads that aren't needed for the actual implementation.
243+
[Uses context_pruning with reason: "exploration complete, pruning unrelated file reads"]
244+
</example>
245+
246+
<example>
247+
After trying multiple approaches that didn't work:
248+
Assistant: I've been trying several approaches to fix this issue. Let me prune the failed attempts to keep focus on the working solution.
249+
[Uses context_pruning with reason: "pruning failed iteration attempts, keeping working solution context"]
250+
</example>`,
225251
args: {
226252
reason: tool.schema.string().optional().describe(
227253
"Brief reason for triggering pruning (e.g., 'task complete', 'switching focus')"
@@ -238,7 +264,7 @@ const plugin: Plugin = (async (ctx) => {
238264
return "No prunable tool outputs found. Context is already optimized."
239265
}
240266

241-
return `Context pruning complete. Pruned ${result.prunedCount} tool outputs (~${formatTokenCount(result.tokensSaved)} tokens saved).`
267+
return janitor.formatPruningResultForTool(result)
242268
},
243269
}),
244270
} : undefined,

lib/janitor.ts

Lines changed: 104 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export class Janitor {
7070
* Convenience method for idle-triggered pruning (sends notification automatically)
7171
*/
7272
async runOnIdle(sessionID: string, strategies: PruningStrategy[]): Promise<void> {
73-
const result = await this.runWithStrategies(sessionID, strategies, { trigger: 'idle' })
73+
await this.runWithStrategies(sessionID, strategies, { trigger: 'idle' })
7474
// Notification is handled inside runWithStrategies
7575
}
7676

@@ -196,7 +196,7 @@ export class Janitor {
196196
// ============================================================
197197
let llmPrunedIds: string[] = []
198198

199-
if (strategies.includes('llm-analysis')) {
199+
if (strategies.includes('ai-analysis')) {
200200
// Filter out duplicates and protected tools
201201
const protectedToolCallIds: string[] = []
202202
const prunableToolCallIds = unprunedToolCallIds.filter(id => {
@@ -253,7 +253,6 @@ export class Janitor {
253253
const analysisPrompt = buildAnalysisPrompt(
254254
prunableToolCallIds,
255255
sanitizedMessages,
256-
this.protectedTools,
257256
allPrunedSoFar,
258257
protectedToolCallIds,
259258
options.reason
@@ -342,8 +341,8 @@ export class Janitor {
342341
this.statsState.set(sessionID, sessionStats)
343342

344343
// Determine notification mode based on which strategies ran
345-
const hasLlmAnalysis = strategies.includes('llm-analysis')
346-
344+
const hasLlmAnalysis = strategies.includes('ai-analysis')
345+
347346
if (hasLlmAnalysis) {
348347
await this.sendSmartModeNotification(
349348
sessionID,
@@ -377,13 +376,13 @@ export class Janitor {
377376
const keptCount = candidateCount - prunedCount
378377
const hasBoth = deduplicatedIds.length > 0 && llmPrunedIds.length > 0
379378
const breakdown = hasBoth ? ` (${deduplicatedIds.length} duplicate, ${llmPrunedIds.length} llm)` : ""
380-
379+
381380
// Build log metadata
382381
const logMeta: Record<string, any> = { trigger: options.trigger }
383382
if (options.reason) {
384383
logMeta.reason = options.reason
385384
}
386-
385+
387386
this.logger.info("janitor", `Pruned ${prunedCount}/${candidateCount} tools${breakdown}, ${keptCount} kept (~${formatTokenCount(tokensSaved)} tokens)`, logMeta)
388387

389388
return {
@@ -568,6 +567,73 @@ export class Janitor {
568567
return toolsSummary
569568
}
570569

570+
/**
571+
* Group deduplication details by tool type
572+
* Shared helper used by notifications and tool output formatting
573+
*/
574+
private groupDeduplicationDetails(
575+
deduplicationDetails: Map<string, any>
576+
): Map<string, Array<{ count: number, key: string }>> {
577+
const grouped = new Map<string, Array<{ count: number, key: string }>>()
578+
579+
for (const [_, details] of deduplicationDetails) {
580+
const { toolName, parameterKey, duplicateCount } = details
581+
if (!grouped.has(toolName)) {
582+
grouped.set(toolName, [])
583+
}
584+
grouped.get(toolName)!.push({
585+
count: duplicateCount,
586+
key: this.shortenPath(parameterKey)
587+
})
588+
}
589+
590+
return grouped
591+
}
592+
593+
/**
594+
* Format grouped deduplication results as lines
595+
* Shared helper for building deduplication summaries
596+
*/
597+
private formatDeduplicationLines(
598+
grouped: Map<string, Array<{ count: number, key: string }>>,
599+
indent: string = ' '
600+
): string[] {
601+
const lines: string[] = []
602+
603+
for (const [toolName, items] of grouped.entries()) {
604+
for (const item of items) {
605+
const removedCount = item.count - 1
606+
lines.push(`${indent}${toolName}: ${item.key} (${removedCount}× duplicate)`)
607+
}
608+
}
609+
610+
return lines
611+
}
612+
613+
/**
614+
* Format tool summary (from buildToolsSummary) as lines
615+
* Shared helper for building LLM-pruned summaries
616+
*/
617+
private formatToolSummaryLines(
618+
toolsSummary: Map<string, string[]>,
619+
indent: string = ' '
620+
): string[] {
621+
const lines: string[] = []
622+
623+
for (const [toolName, params] of toolsSummary.entries()) {
624+
if (params.length === 1) {
625+
lines.push(`${indent}${toolName}: ${params[0]}`)
626+
} else if (params.length > 1) {
627+
lines.push(`${indent}${toolName} (${params.length}):`)
628+
for (const param of params) {
629+
lines.push(`${indent} ${param}`)
630+
}
631+
}
632+
}
633+
634+
return lines
635+
}
636+
571637
/**
572638
* Send minimal summary notification (just tokens saved and count)
573639
*/
@@ -625,21 +691,10 @@ export class Janitor {
625691
}
626692
message += '\n'
627693

628-
// Group by tool type
629-
const grouped = new Map<string, Array<{ count: number, key: string }>>()
694+
// Group by tool type using shared helper
695+
const grouped = this.groupDeduplicationDetails(deduplicationDetails)
630696

631-
for (const [_, details] of deduplicationDetails) {
632-
const { toolName, parameterKey, duplicateCount } = details
633-
if (!grouped.has(toolName)) {
634-
grouped.set(toolName, [])
635-
}
636-
grouped.get(toolName)!.push({
637-
count: duplicateCount,
638-
key: this.shortenPath(parameterKey)
639-
})
640-
}
641-
642-
// Display grouped results
697+
// Display grouped results (with UI-specific formatting: total dupes header, limit to 5)
643698
for (const [toolName, items] of grouped.entries()) {
644699
const totalDupes = items.reduce((sum, item) => sum + (item.count - 1), 0)
645700
message += `\n${toolName} (${totalDupes} duplicate${totalDupes > 1 ? 's' : ''}):\n`
@@ -657,6 +712,33 @@ export class Janitor {
657712
await this.sendIgnoredMessage(sessionID, message.trim())
658713
}
659714

715+
/**
716+
* Format pruning result for tool output (returned to AI)
717+
* Uses shared helpers for consistency with UI notifications
718+
*/
719+
formatPruningResultForTool(result: PruningResult): string {
720+
const lines: string[] = []
721+
lines.push(`Context pruning complete. Pruned ${result.prunedCount} tool outputs.`)
722+
lines.push('')
723+
724+
// Section 1: Deduplicated tools
725+
if (result.deduplicatedIds.length > 0 && result.deduplicationDetails.size > 0) {
726+
lines.push(`Duplicates removed (${result.deduplicatedIds.length}):`)
727+
const grouped = this.groupDeduplicationDetails(result.deduplicationDetails)
728+
lines.push(...this.formatDeduplicationLines(grouped))
729+
lines.push('')
730+
}
731+
732+
// Section 2: LLM-pruned tools
733+
if (result.llmPrunedIds.length > 0) {
734+
lines.push(`Semantically pruned (${result.llmPrunedIds.length}):`)
735+
const toolsSummary = this.buildToolsSummary(result.llmPrunedIds, result.toolMetadata)
736+
lines.push(...this.formatToolSummaryLines(toolsSummary))
737+
}
738+
739+
return lines.join('\n').trim()
740+
}
741+
660742
/**
661743
* Smart mode notification - shows both deduplication and LLM analysis results
662744
*/
@@ -695,20 +777,7 @@ export class Janitor {
695777
// Section 1: Deduplicated tools
696778
if (deduplicatedIds.length > 0 && deduplicationDetails) {
697779
message += `\n📦 Duplicates removed (${deduplicatedIds.length}):\n`
698-
699-
// Group by tool type
700-
const grouped = new Map<string, Array<{ count: number, key: string }>>()
701-
702-
for (const [_, details] of deduplicationDetails) {
703-
const { toolName, parameterKey, duplicateCount } = details
704-
if (!grouped.has(toolName)) {
705-
grouped.set(toolName, [])
706-
}
707-
grouped.get(toolName)!.push({
708-
count: duplicateCount,
709-
key: this.shortenPath(parameterKey)
710-
})
711-
}
780+
const grouped = this.groupDeduplicationDetails(deduplicationDetails)
712781

713782
for (const [toolName, items] of grouped.entries()) {
714783
message += ` ${toolName}:\n`
@@ -722,8 +791,6 @@ export class Janitor {
722791
// Section 2: LLM-pruned tools
723792
if (llmPrunedIds.length > 0) {
724793
message += `\n🤖 LLM analysis (${llmPrunedIds.length}):\n`
725-
726-
// Use buildToolsSummary logic
727794
const toolsSummary = this.buildToolsSummary(llmPrunedIds, toolMetadata)
728795

729796
for (const [toolName, params] of toolsSummary.entries()) {

lib/prompt.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,6 @@ function minimizeMessages(messages: any[], alreadyPrunedIds?: string[], protecte
106106
export function buildAnalysisPrompt(
107107
unprunedToolCallIds: string[],
108108
messages: any[],
109-
protectedTools: string[],
110109
alreadyPrunedIds?: string[],
111110
protectedToolCallIds?: string[],
112111
reason?: string // Optional reason from tool call

0 commit comments

Comments
 (0)