Skip to content

Commit 72fcc07

Browse files
authored
Merge pull request #119 from Opencode-DCP/nudge-frequency-and-config-update
implement nudge and config update
2 parents b47fec3 + 07fa3c4 commit 72fcc07

File tree

9 files changed

+117
-50
lines changed

9 files changed

+117
-50
lines changed

.github/workflows/pr-checks.yml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,7 @@ name: PR Checks
22

33
on:
44
pull_request:
5-
branches: [main, master]
6-
push:
7-
branches: [main, master]
5+
branches: [master, dev]
86

97
jobs:
108
validate:

README.md

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,15 @@ Restart OpenCode. The plugin will automatically start optimizing your sessions.
2323

2424
## How Pruning Works
2525

26-
DCP uses two complementary techniques:
26+
DCP uses multiple strategies to reduce context size:
2727

28-
**Automatic Deduplication**Silently identifies repeated tool calls (e.g., reading the same file multiple times) and keeps only the most recent output. Runs on every request with zero LLM cost.
28+
**Deduplication**Identifies repeated tool calls (e.g., reading the same file multiple times) and keeps only the most recent output. Runs automatically on every request with zero LLM cost.
2929

30-
**AI Analysis**Uses a language model to semantically analyze conversation context and identify tool outputs that are no longer relevant to the current task.
30+
**Prune Thinking Blocks**Removes LLM thinking/reasoning blocks from the conversation history.
3131

32-
## Context Pruning Tool
32+
**On Idle Analysis** — Uses a language model to semantically analyze conversation context during idle periods and identify tool outputs that are no longer relevant.
3333

34-
When `strategies.onTool` is enabled, DCP exposes a `prune` tool to Opencode that the AI can call to trigger pruning on demand.
35-
36-
Adjust `nudgeFreq` to control how aggressively the AI is prompted to prune — lower values trigger reminders sooner and more often.
37-
38-
## How It Works
34+
**Prune Tool** — Exposes a `prune` tool that the AI can call to manually trigger pruning when it determines context cleanup is needed.
3935

4036
Your session history is never modified. DCP replaces pruned outputs with a placeholder before sending requests to your LLM.
4137

@@ -49,35 +45,67 @@ LLM providers like Anthropic and OpenAI cache prompts based on exact prefix matc
4945

5046
DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.jsonc`), created automatically on first run.
5147

52-
### Options
53-
54-
| Option | Default | Description |
55-
|--------|---------|-------------|
56-
| `enabled` | `true` | Enable/disable the plugin |
57-
| `debug` | `false` | Log to `~/.config/opencode/logs/dcp/` |
58-
| `model` | (session) | Model for analysis (e.g., `"anthropic/claude-haiku-4-5"`) |
59-
| `showModelErrorToasts` | `true` | Show notifications on model fallback |
60-
| `showUpdateToasts` | `true` | Show notifications when a new version is available |
61-
| `strictModelSelection` | `false` | Only run AI analysis with session or configured model (disables fallback models) |
62-
| `pruningSummary` | `"detailed"` | `"off"`, `"minimal"`, or `"detailed"` |
63-
| `nudgeFreq` | `10` | How often to remind AI to prune (lower = more frequent) |
64-
| `protectedTools` | `["task", "todowrite", "todoread", "prune", "batch", "write", "edit"]` | Tools that are never pruned |
65-
| `strategies.onIdle` | `["ai-analysis"]` | Strategies for automatic pruning |
66-
| `strategies.onTool` | `["ai-analysis"]` | Strategies when AI calls `prune` |
67-
68-
**Strategies:** `"ai-analysis"` uses LLM to identify prunable outputs. Empty array disables that trigger. Deduplication runs automatically on every request.
48+
<details>
49+
<summary><strong>Default Configuration</strong> (click to expand)</summary>
6950

7051
```jsonc
7152
{
53+
// Enable or disable the plugin
7254
"enabled": true,
55+
// Enable debug logging to ~/.config/opencode/logs/dcp/
56+
"debug": false,
57+
// Show toast notifications when a new version is available
58+
"showUpdateToasts": true,
59+
// Summary display: "off", "minimal", or "detailed"
60+
"pruningSummary": "detailed",
61+
// Strategies for pruning tokens from chat history
7362
"strategies": {
74-
"onIdle": ["ai-analysis"],
75-
"onTool": ["ai-analysis"]
76-
},
77-
"protectedTools": ["task", "todowrite", "todoread", "prune", "batch", "write", "edit"]
63+
// Remove duplicate tool calls (same tool with same arguments)
64+
"deduplication": {
65+
"enabled": true,
66+
// Additional tools to protect from pruning
67+
"protectedTools": []
68+
},
69+
// Remove thinking/reasoning LLM blocks
70+
"pruneThinkingBlocks": {
71+
"enabled": false
72+
},
73+
// (Legacy) Run an LLM to analyze what tool calls are no longer relevant on idle
74+
"onIdle": {
75+
"enabled": false,
76+
// Override model for analysis (format: "provider/model")
77+
// "model": "anthropic/claude-haiku-4-5",
78+
// Show toast notifications when model selection fails
79+
"showModelErrorToasts": true,
80+
// When true, fallback models are not permitted
81+
"strictModelSelection": false,
82+
// Additional tools to protect from pruning
83+
"protectedTools": []
84+
},
85+
// Exposes a prune tool to your LLM to call when it determines pruning is necessary
86+
"pruneTool": {
87+
"enabled": false,
88+
// Additional tools to protect from pruning
89+
"protectedTools": [],
90+
// Nudge the LLM to use the prune tool (every <frequency> tool results)
91+
"nudge": {
92+
"enabled": true,
93+
"frequency": 10
94+
}
95+
}
96+
}
7897
}
7998
```
8099

100+
</details>
101+
102+
### Protected Tools
103+
104+
By default, these tools are always protected from pruning across all strategies:
105+
`task`, `todowrite`, `todoread`, `prune`, `batch`, `write`, `edit`
106+
107+
The `protectedTools` arrays in each strategy add to this default list.
108+
81109
### Config Precedence
82110

83111
Settings are merged in order: **Defaults****Global** (`~/.config/opencode/dcp.jsonc`) → **Project** (`.opencode/dcp.jsonc`). Each level overrides the previous, so project settings take priority over global, which takes priority over defaults.

lib/config.ts

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,15 @@ export interface OnIdle {
2121
protectedTools: string[]
2222
}
2323

24+
export interface PruneToolNudge {
25+
enabled: boolean
26+
frequency: number
27+
}
28+
2429
export interface PruneTool {
2530
enabled: boolean
2631
protectedTools: string[]
27-
nudgeFrequency: number
32+
nudge: PruneToolNudge
2833
}
2934

3035
export interface PluginConfig {
@@ -68,7 +73,9 @@ export const VALID_CONFIG_KEYS = new Set([
6873
'strategies.pruneTool',
6974
'strategies.pruneTool.enabled',
7075
'strategies.pruneTool.protectedTools',
71-
'strategies.pruneTool.nudgeFrequency',
76+
'strategies.pruneTool.nudge',
77+
'strategies.pruneTool.nudge.enabled',
78+
'strategies.pruneTool.nudge.frequency',
7279
])
7380

7481
// Extract all key paths from a config object for validation
@@ -160,8 +167,13 @@ function validateConfigTypes(config: Record<string, any>): ValidationError[] {
160167
if (strategies.pruneTool.protectedTools !== undefined && !Array.isArray(strategies.pruneTool.protectedTools)) {
161168
errors.push({ key: 'strategies.pruneTool.protectedTools', expected: 'string[]', actual: typeof strategies.pruneTool.protectedTools })
162169
}
163-
if (strategies.pruneTool.nudgeFrequency !== undefined && typeof strategies.pruneTool.nudgeFrequency !== 'number') {
164-
errors.push({ key: 'strategies.pruneTool.nudgeFrequency', expected: 'number', actual: typeof strategies.pruneTool.nudgeFrequency })
170+
if (strategies.pruneTool.nudge) {
171+
if (strategies.pruneTool.nudge.enabled !== undefined && typeof strategies.pruneTool.nudge.enabled !== 'boolean') {
172+
errors.push({ key: 'strategies.pruneTool.nudge.enabled', expected: 'boolean', actual: typeof strategies.pruneTool.nudge.enabled })
173+
}
174+
if (strategies.pruneTool.nudge.frequency !== undefined && typeof strategies.pruneTool.nudge.frequency !== 'number') {
175+
errors.push({ key: 'strategies.pruneTool.nudge.frequency', expected: 'number', actual: typeof strategies.pruneTool.nudge.frequency })
176+
}
165177
}
166178
}
167179
}
@@ -226,18 +238,21 @@ const defaultConfig: PluginConfig = {
226238
protectedTools: [...DEFAULT_PROTECTED_TOOLS]
227239
},
228240
pruneThinkingBlocks: {
229-
enabled: true
241+
enabled: false
230242
},
231243
onIdle: {
232-
enabled: true,
244+
enabled: false,
233245
showModelErrorToasts: true,
234246
strictModelSelection: false,
235247
protectedTools: [...DEFAULT_PROTECTED_TOOLS]
236248
},
237249
pruneTool: {
238250
enabled: false,
239251
protectedTools: [...DEFAULT_PROTECTED_TOOLS],
240-
nudgeFrequency: 10
252+
nudge: {
253+
enabled: true,
254+
frequency: 10
255+
}
241256
}
242257
}
243258
}
@@ -309,11 +324,11 @@ function createDefaultConfig(): void {
309324
},
310325
// Remove thinking/reasoning LLM blocks
311326
"pruneThinkingBlocks": {
312-
"enabled": true
327+
"enabled": false
313328
},
314-
// Run an LLM to analyze what tool calls are no longer relevant on idle
329+
// (Legacy) Run an LLM to analyze what tool calls are no longer relevant on idle
315330
"onIdle": {
316-
"enabled": true,
331+
"enabled": false,
317332
// Override model for analysis (format: "provider/model")
318333
// "model": "anthropic/claude-haiku-4-5",
319334
// Show toast notifications when model selection fails
@@ -328,8 +343,11 @@ function createDefaultConfig(): void {
328343
"enabled": false,
329344
// Additional tools to protect from pruning
330345
"protectedTools": [],
331-
// How often to nudge the AI to prune (every N tool results, 0 = disabled)
332-
"nudgeFrequency": 10
346+
// Nudge the LLM to use the prune tool (every <frequency> tool results)
347+
"nudge": {
348+
"enabled": true,
349+
"frequency": 10
350+
}
333351
}
334352
}
335353
}
@@ -401,7 +419,10 @@ function mergeStrategies(
401419
...(override.pruneTool?.protectedTools ?? [])
402420
])
403421
],
404-
nudgeFrequency: override.pruneTool?.nudgeFrequency ?? base.pruneTool.nudgeFrequency
422+
nudge: {
423+
enabled: override.pruneTool?.nudge?.enabled ?? base.pruneTool.nudge.enabled,
424+
frequency: override.pruneTool?.nudge?.frequency ?? base.pruneTool.nudge.frequency
425+
}
405426
}
406427
}
407428
}
@@ -421,7 +442,8 @@ function deepCloneConfig(config: PluginConfig): PluginConfig {
421442
},
422443
pruneTool: {
423444
...config.strategies.pruneTool,
424-
protectedTools: [...config.strategies.pruneTool.protectedTools]
445+
protectedTools: [...config.strategies.pruneTool.protectedTools],
446+
nudge: { ...config.strategies.pruneTool.nudge }
425447
}
426448
}
427449
}

lib/hooks.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export function createChatMessageTransformHandler(
1818
output: { messages: WithParts[] }
1919
) => {
2020
checkSession(state, logger, output.messages);
21-
syncToolCache(state, logger, output.messages);
21+
syncToolCache(state, config, logger, output.messages);
22+
2223

2324
deduplicate(state, logger, config, output.messages)
2425

lib/messages/prune.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ import type { Logger } from "../logger"
33
import type { PluginConfig } from "../config"
44
import { buildToolIdList } from "../utils"
55
import { getLastUserMessage, extractParameterKey } from "./utils"
6+
import { loadPrompt } from "../prompt"
67

78
const PRUNED_TOOL_OUTPUT_REPLACEMENT = '[Output removed to save context - information superseded or no longer needed]'
9+
const NUDGE_STRING = loadPrompt("nudge")
810

911
const buildPrunableToolsList = (
1012
state: SessionState,
@@ -45,6 +47,12 @@ export const insertPruneToolContext = (
4547

4648
const prunableToolsList = buildPrunableToolsList(state, config, logger, messages)
4749

50+
let nudgeString = ""
51+
if (config.strategies.pruneTool.nudge.enabled && state.nudgeCounter >= config.strategies.pruneTool.nudge.frequency) {
52+
logger.info("Inserting prune nudge message")
53+
nudgeString = "\n" + NUDGE_STRING
54+
}
55+
4856
const userMessage: WithParts = {
4957
info: {
5058
id: "msg_01234567890123456789012345",
@@ -63,7 +71,7 @@ export const insertPruneToolContext = (
6371
sessionID: lastUserMessage.info.sessionID,
6472
messageID: "msg_01234567890123456789012345",
6573
type: "text",
66-
text: prunableToolsList,
74+
text: prunableToolsList + nudgeString,
6775
}
6876
]
6977
}

lib/state/state.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ export function createSessionState(): SessionState {
3838
pruneTokenCounter: 0,
3939
totalPruneTokens: 0,
4040
},
41-
toolParameters: new Map<string, ToolParameterEntry>()
41+
toolParameters: new Map<string, ToolParameterEntry>(),
42+
nudgeCounter: 0
4243
}
4344
}
4445

@@ -52,6 +53,7 @@ export function resetSessionState(state: SessionState): void {
5253
totalPruneTokens: 0,
5354
}
5455
state.toolParameters.clear()
56+
state.nudgeCounter = 0
5557
}
5658

5759
export async function ensureSessionInitialized(

lib/state/tool-cache.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { SessionState, ToolStatus, WithParts } from "./index"
22
import type { Logger } from "../logger"
3+
import { PluginConfig } from "../config"
34

45
const MAX_TOOL_CACHE_SIZE = 500
56

@@ -10,6 +11,7 @@ const MAX_TOOL_CACHE_SIZE = 500
1011
*/
1112
export async function syncToolCache(
1213
state: SessionState,
14+
config: PluginConfig,
1315
logger: Logger,
1416
messages: WithParts[],
1517
): Promise<void> {
@@ -30,6 +32,10 @@ export async function syncToolCache(
3032
error: part.state.status === "error" ? part.state.error : undefined,
3133
}
3234
)
35+
36+
if (!config.strategies.pruneTool.protectedTools.includes(part.tool)) {
37+
state.nudgeCounter++
38+
}
3339
}
3440
}
3541

lib/state/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,5 @@ export interface SessionState {
2828
prune: Prune
2929
stats: SessionStats
3030
toolParameters: Map<string, ToolParameterEntry>
31+
nudgeCounter: number
3132
}

lib/strategies/prune-tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ export function createPruneTool(
102102
)
103103
state.stats.totalPruneTokens += state.stats.pruneTokenCounter
104104
state.stats.pruneTokenCounter = 0
105+
state.nudgeCounter = 0
105106

106107
return formatPruningResultForTool(
107108
pruneToolIds,

0 commit comments

Comments
 (0)