Skip to content

Commit c17fa92

Browse files
committed
Feat: Add reason parameter to prune tool for categorizing pruning actions
Allows LLM to specify why it's pruning (completion, noise, consolidation) as the first element of the ids array. Reason is displayed in UI notifications but hidden from tool output to avoid redundancy.
1 parent e57a3a8 commit c17fa92

File tree

5 files changed

+78
-37
lines changed

5 files changed

+78
-37
lines changed

lib/core/janitor.ts

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { z } from "zod"
22
import type { Logger } from "../logger"
33
import type { PruningStrategy } from "../config"
44
import type { PluginState } from "../state"
5-
import type { ToolMetadata } from "../fetch-wrapper/types"
5+
import type { ToolMetadata, PruneReason, SessionStats, GCStats, PruningResult } from "../fetch-wrapper/types"
66
import { findCurrentAgent } from "../hooks"
77
import { buildAnalysisPrompt } from "./prompt"
88
import { selectModel, extractModelFromSession } from "../model-selector"
@@ -14,25 +14,7 @@ import {
1414
type NotificationContext
1515
} from "../ui/notification"
1616

17-
export interface SessionStats {
18-
totalToolsPruned: number
19-
totalTokensSaved: number
20-
totalGCTokens: number
21-
totalGCTools: number
22-
}
23-
24-
export interface GCStats {
25-
tokensCollected: number
26-
toolsDeduped: number
27-
}
28-
29-
export interface PruningResult {
30-
prunedCount: number
31-
tokensSaved: number
32-
llmPrunedIds: string[]
33-
toolMetadata: Map<string, ToolMetadata>
34-
sessionStats: SessionStats
35-
}
17+
export type { SessionStats, GCStats, PruningResult }
3618

3719
export interface PruningOptions {
3820
reason?: string

lib/fetch-wrapper/types.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,34 @@ export interface PrunedIdData {
4343
allSessions: any
4444
allPrunedIds: Set<string>
4545
}
46+
47+
/** The 3 scenarios that trigger explicit LLM pruning */
48+
export type PruneReason = "completion" | "noise" | "consolidation"
49+
50+
/** Human-readable labels for prune reasons */
51+
export const PRUNE_REASON_LABELS: Record<PruneReason, string> = {
52+
completion: "Task Complete",
53+
noise: "Noise Removal",
54+
consolidation: "Consolidation"
55+
}
56+
57+
export interface SessionStats {
58+
totalToolsPruned: number
59+
totalTokensSaved: number
60+
totalGCTokens: number
61+
totalGCTools: number
62+
}
63+
64+
export interface GCStats {
65+
tokensCollected: number
66+
toolsDeduped: number
67+
}
68+
69+
export interface PruningResult {
70+
prunedCount: number
71+
tokensSaved: number
72+
llmPrunedIds: string[]
73+
toolMetadata: Map<string, ToolMetadata>
74+
sessionStats: SessionStats
75+
reason?: PruneReason
76+
}

lib/prompts/tool.txt

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
Prunes tool outputs from context to manage conversation size and reduce noise.
22

33
## IMPORTANT: The Prunable List
4-
A list of available tool outputs (with numeric IDs) is maintained for you in the SYSTEM PROMPT at the beginning of the context. This list is always up-to-date. Use these IDs to select tools to prune.
4+
A `<prunable-tools>` list is injected into user messages showing available tool outputs you can prune. Each line has the format `ID: tool, parameter` (e.g., `20: read, /path/to/file.ts`). Use these numeric IDs to select which tools to prune.
55

66
## CRITICAL: When and How to Prune
77

8-
You must use this tool in three specific scenarios. The rules for distillation (summarizing findings) differ for each.
8+
You must use this tool in three specific scenarios. The rules for distillation (summarizing findings) differ for each. **You must specify the reason as the first element of the `ids` array** to indicate which scenario applies.
99

10-
### 1. Task Completion (Clean Up)
10+
### 1. Task Completion (Clean Up) — reason: `completion`
1111
**When:** You have successfully completed a specific unit of work (e.g., fixed a bug, wrote a file, answered a question).
1212
**Action:** Prune the tools used for that task.
1313
**Distillation:** NOT REQUIRED. Since the task is done, the raw data is no longer needed. Simply state that the task is complete.
1414

15-
### 2. Removing Noise (Garbage Collection)
15+
### 2. Removing Noise (Garbage Collection) — reason: `noise`
1616
**When:** You have read files or run commands that turned out to be irrelevant, unhelpful, or outdated (meaning later tools have provided fresher, more valid information).
1717
**Action:** Prune these specific tool outputs immediately.
1818
**Distillation:** FORBIDDEN. Do not pollute the context by summarizing useless information. Just cut it out.
1919

20-
### 3. Context Conservation (Research & Consolidation)
20+
### 3. Context Conservation (Research & Consolidation) — reason: `consolidation`
2121
**When:** You have gathered useful information. Prune frequently as you work (e.g., after reading a few files), rather than waiting for a "long" phase to end.
2222
**Action:** Convert raw data into distilled knowledge. This allows you to discard large outputs (like full file reads) while keeping only the specific parts you need (like a single function signature or constant).
2323
**Distillation:** MANDATORY. Before pruning, you *must* explicitly summarize the key findings from *every* tool you plan to prune.
@@ -36,7 +36,7 @@ You must use this tool in three specific scenarios. The rules for distillation (
3636
<example_noise>
3737
Assistant: [Reads 'wrong_file.ts']
3838
This file isn't relevant to the auth system. I'll remove it to clear the context.
39-
[Uses prune with ids: [5]]
39+
[Uses prune with ids: ["noise", 5]]
4040
</example_noise>
4141

4242
<example_consolidation>
@@ -46,11 +46,11 @@ I have analyzed the configuration. Here is the distillation:
4646
- 'db.ts' connects to mongo:27017.
4747
- The other 3 files were defaults.
4848
I have preserved the signals above, so I am now pruning the raw reads.
49-
[Uses prune with ids: [10, 11, 12, 13, 14]]
49+
[Uses prune with ids: ["consolidation", 10, 11, 12, 13, 14]]
5050
</example_consolidation>
5151

5252
<example_completion>
5353
Assistant: [Runs tests, they pass]
5454
The tests passed. The feature is verified.
55-
[Uses prune with ids: [20, 21]]
55+
[Uses prune with ids: ["completion", 20, 21]]
5656
</example_completion>

lib/pruning-tool.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { tool } from "@opencode-ai/plugin"
22
import type { PluginState } from "./state"
33
import type { PluginConfig } from "./config"
44
import type { ToolTracker } from "./fetch-wrapper/tool-tracker"
5-
import type { ToolMetadata } from "./fetch-wrapper/types"
5+
import type { ToolMetadata, PruneReason } from "./fetch-wrapper/types"
66
import { resetToolTrackerCount } from "./fetch-wrapper/tool-tracker"
77
import { isSubagentSession, findCurrentAgent } from "./hooks"
88
import { getActualId } from "./state/id-mapping"
@@ -38,8 +38,13 @@ export function createPruningTool(
3838
return tool({
3939
description: TOOL_DESCRIPTION,
4040
args: {
41-
ids: tool.schema.array(tool.schema.number()).describe(
42-
"Array of numeric IDs to prune from the <prunable-tools> list"
41+
ids: tool.schema.array(
42+
tool.schema.union([
43+
tool.schema.enum(["completion", "noise", "consolidation"]),
44+
tool.schema.number()
45+
])
46+
).describe(
47+
"First element is the reason ('completion', 'noise', 'consolidation'), followed by numeric IDs to prune"
4348
),
4449
},
4550
async execute(args, toolCtx) {
@@ -54,9 +59,26 @@ export function createPruningTool(
5459
return "No IDs provided. Check the <prunable-tools> list for available IDs to prune."
5560
}
5661

62+
// Parse reason from first element, numeric IDs from the rest
63+
const firstElement = args.ids[0]
64+
const validReasons = ["completion", "noise", "consolidation"] as const
65+
let reason: PruneReason | undefined
66+
let numericIds: number[]
67+
68+
if (typeof firstElement === "string" && validReasons.includes(firstElement as any)) {
69+
reason = firstElement as PruneReason
70+
numericIds = args.ids.slice(1).filter((id): id is number => typeof id === "number")
71+
} else {
72+
numericIds = args.ids.filter((id): id is number => typeof id === "number")
73+
}
74+
75+
if (numericIds.length === 0) {
76+
return "No numeric IDs provided. Format: [reason, id1, id2, ...] where reason is 'completion', 'noise', or 'consolidation'."
77+
}
78+
5779
await ensureSessionRestored(state, sessionId, logger)
5880

59-
const prunedIds = args.ids
81+
const prunedIds = numericIds
6082
.map(numId => getActualId(sessionId, numId))
6183
.filter((id): id is string => id !== undefined)
6284

@@ -114,7 +136,8 @@ export function createPruningTool(
114136
aiPrunedIds: prunedIds,
115137
toolMetadata,
116138
gcPending: null,
117-
sessionStats
139+
sessionStats,
140+
reason
118141
}, currentAgent)
119142

120143
toolTracker.skipNextIdle = true
@@ -128,7 +151,8 @@ export function createPruningTool(
128151
tokensSaved,
129152
llmPrunedIds: prunedIds,
130153
toolMetadata,
131-
sessionStats
154+
sessionStats,
155+
reason
132156
}
133157

134158
return formatPruningResultForTool(result, ctx.workingDirectory)

lib/ui/notification.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Logger } from "../logger"
22
import type { SessionStats, GCStats } from "../core/janitor"
3-
import type { ToolMetadata } from "../fetch-wrapper/types"
3+
import type { ToolMetadata, PruneReason } from "../fetch-wrapper/types"
4+
import { PRUNE_REASON_LABELS } from "../fetch-wrapper/types"
45
import { formatTokenCount } from "../tokenizer"
56
import { formatPrunedItemsList } from "./display-utils"
67

@@ -24,6 +25,7 @@ export interface NotificationData {
2425
toolMetadata: Map<string, ToolMetadata>
2526
gcPending: GCStats | null
2627
sessionStats: SessionStats | null
28+
reason?: PruneReason
2729
}
2830

2931
export async function sendUnifiedNotification(
@@ -77,7 +79,8 @@ export async function sendIgnoredMessage(
7779

7880
function buildMinimalMessage(data: NotificationData): string {
7981
const { justNowTokens, totalTokens } = calculateStats(data)
80-
return formatStatsHeader(totalTokens, justNowTokens)
82+
const reasonSuffix = data.reason ? ` [${PRUNE_REASON_LABELS[data.reason]}]` : ''
83+
return formatStatsHeader(totalTokens, justNowTokens) + reasonSuffix
8184
}
8285

8386
function buildDetailedMessage(data: NotificationData, workingDirectory?: string): string {
@@ -87,7 +90,8 @@ function buildDetailedMessage(data: NotificationData, workingDirectory?: string)
8790

8891
if (data.aiPrunedCount > 0) {
8992
const justNowTokensStr = `~${formatTokenCount(justNowTokens)}`
90-
message += `\n\n▣ Pruned tools (${justNowTokensStr})`
93+
const reasonLabel = data.reason ? ` — ${PRUNE_REASON_LABELS[data.reason]}` : ''
94+
message += `\n\n▣ Pruned tools (${justNowTokensStr})${reasonLabel}`
9195

9296
const itemLines = formatPrunedItemsList(data.aiPrunedIds, data.toolMetadata, workingDirectory)
9397
message += '\n' + itemLines.join('\n')

0 commit comments

Comments
 (0)