Skip to content

Commit 3d27337

Browse files
authored
Merge pull request #30 from Tarquinen/fix/deduplicator-param-access-and-batch-exclusion
fix: deduplicator param access and batch tool exclusion
2 parents 38796d3 + 005e463 commit 3d27337

File tree

6 files changed

+92
-88
lines changed

6 files changed

+92
-88
lines changed

README.md

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,28 @@ Automatically reduces token usage in OpenCode by removing obsolete tool outputs
66

77
![DCP in action](dcp-demo.png)
88

9-
## Pruning Strategies
10-
11-
DCP implements two complementary strategies:
12-
13-
**Deduplication** — Fast, zero-cost pruning that identifies repeated tool calls (e.g., reading the same file multiple times) and keeps only the most recent output. Runs instantly with no LLM calls.
14-
15-
**AI Analysis** — Uses a language model to semantically analyze conversation context and identify tool outputs that are no longer relevant to the current task. More thorough but incurs LLM cost.
16-
179
## Installation
1810

1911
Add to your OpenCode config:
2012

2113
```jsonc
2214
// opencode.jsonc
2315
{
24-
"plugins": ["@tarquinen/[email protected].17"]
16+
"plugins": ["@tarquinen/[email protected].18"]
2517
}
2618
```
2719

2820
When a new version is available, DCP will show a toast notification. Update by changing the version number in your config.
2921

3022
Restart OpenCode. The plugin will automatically start optimizing your sessions.
3123

32-
> **Note:** Project `plugin` arrays override global completely—include all desired plugins in project config if using both.
24+
## Pruning Strategies
25+
26+
DCP implements two complementary strategies:
3327

28+
**Deduplication** — Fast, zero-cost pruning that identifies repeated tool calls (e.g., reading the same file multiple times) and keeps only the most recent output. Runs instantly with no LLM calls.
29+
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. More thorough but incurs LLM cost.
3431
## How It Works
3532

3633
DCP is **non-destructive**—pruning state is kept in memory only. When requests go to your LLM, DCP replaces pruned outputs with a placeholder; original session data stays intact.

lib/deduplicator.ts

Lines changed: 2 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { extractParameterKey } from "./display-utils"
2+
13
export interface DuplicateDetectionResult {
24
duplicateIds: string[] // IDs to prune (older duplicates)
35
deduplicationDetails: Map<string, {
@@ -85,76 +87,3 @@ function sortObjectKeys(obj: any): any {
8587
}
8688
return sorted
8789
}
88-
89-
export function extractParameterKey(metadata: { tool: string, parameters?: any }): string {
90-
if (!metadata.parameters) return ''
91-
92-
const { tool, parameters } = metadata
93-
94-
if (tool === "read" && parameters.filePath) {
95-
return parameters.filePath
96-
}
97-
if (tool === "write" && parameters.filePath) {
98-
return parameters.filePath
99-
}
100-
if (tool === "edit" && parameters.filePath) {
101-
return parameters.filePath
102-
}
103-
104-
if (tool === "list") {
105-
return parameters.path || '(current directory)'
106-
}
107-
if (tool === "glob") {
108-
if (parameters.pattern) {
109-
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
110-
return `"${parameters.pattern}"${pathInfo}`
111-
}
112-
return '(unknown pattern)'
113-
}
114-
if (tool === "grep") {
115-
if (parameters.pattern) {
116-
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
117-
return `"${parameters.pattern}"${pathInfo}`
118-
}
119-
return '(unknown pattern)'
120-
}
121-
122-
if (tool === "bash") {
123-
if (parameters.description) return parameters.description
124-
if (parameters.command) {
125-
return parameters.command.length > 50
126-
? parameters.command.substring(0, 50) + "..."
127-
: parameters.command
128-
}
129-
}
130-
131-
if (tool === "webfetch" && parameters.url) {
132-
return parameters.url
133-
}
134-
if (tool === "websearch" && parameters.query) {
135-
return `"${parameters.query}"`
136-
}
137-
if (tool === "codesearch" && parameters.query) {
138-
return `"${parameters.query}"`
139-
}
140-
141-
if (tool === "todowrite") {
142-
return `${parameters.todos?.length || 0} todos`
143-
}
144-
if (tool === "todoread") {
145-
return "read todo list"
146-
}
147-
148-
if (tool === "task" && parameters.description) {
149-
return parameters.description
150-
}
151-
if (tool === "batch") {
152-
return `${parameters.tool_calls?.length || 0} parallel tools`
153-
}
154-
155-
const paramStr = JSON.stringify(parameters)
156-
if (paramStr === '{}' || paramStr === '[]' || paramStr === 'null') {
157-
return ''
158-
}
159-
return paramStr.substring(0, 50)
160-
}

lib/display-utils.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Extracts a human-readable key from tool metadata for display purposes.
3+
* Used by both deduplication and AI analysis to show what was pruned.
4+
*/
5+
export function extractParameterKey(metadata: { tool: string, parameters?: any }): string {
6+
if (!metadata.parameters) return ''
7+
8+
const { tool, parameters } = metadata
9+
10+
if (tool === "read" && parameters.filePath) {
11+
return parameters.filePath
12+
}
13+
if (tool === "write" && parameters.filePath) {
14+
return parameters.filePath
15+
}
16+
if (tool === "edit" && parameters.filePath) {
17+
return parameters.filePath
18+
}
19+
20+
if (tool === "list") {
21+
return parameters.path || '(current directory)'
22+
}
23+
if (tool === "glob") {
24+
if (parameters.pattern) {
25+
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
26+
return `"${parameters.pattern}"${pathInfo}`
27+
}
28+
return '(unknown pattern)'
29+
}
30+
if (tool === "grep") {
31+
if (parameters.pattern) {
32+
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
33+
return `"${parameters.pattern}"${pathInfo}`
34+
}
35+
return '(unknown pattern)'
36+
}
37+
38+
if (tool === "bash") {
39+
if (parameters.description) return parameters.description
40+
if (parameters.command) {
41+
return parameters.command.length > 50
42+
? parameters.command.substring(0, 50) + "..."
43+
: parameters.command
44+
}
45+
}
46+
47+
if (tool === "webfetch" && parameters.url) {
48+
return parameters.url
49+
}
50+
if (tool === "websearch" && parameters.query) {
51+
return `"${parameters.query}"`
52+
}
53+
if (tool === "codesearch" && parameters.query) {
54+
return `"${parameters.query}"`
55+
}
56+
57+
if (tool === "todowrite") {
58+
return `${parameters.todos?.length || 0} todos`
59+
}
60+
if (tool === "todoread") {
61+
return "read todo list"
62+
}
63+
64+
if (tool === "task" && parameters.description) {
65+
return parameters.description
66+
}
67+
if (tool === "batch") {
68+
return `${parameters.tool_calls?.length || 0} parallel tools`
69+
}
70+
71+
const paramStr = JSON.stringify(parameters)
72+
if (paramStr === '{}' || paramStr === '[]' || paramStr === 'null') {
73+
return ''
74+
}
75+
return paramStr.substring(0, 50)
76+
}

lib/janitor.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import type { PruningStrategy } from "./config"
44
import { buildAnalysisPrompt } from "./prompt"
55
import { selectModel, extractModelFromSession } from "./model-selector"
66
import { estimateTokensBatch, formatTokenCount } from "./tokenizer"
7-
import { detectDuplicates, extractParameterKey } from "./deduplicator"
7+
import { detectDuplicates } from "./deduplicator"
8+
import { extractParameterKey } from "./display-utils"
89

910
export interface SessionStats {
1011
totalToolsPruned: number
@@ -108,7 +109,7 @@ export class Janitor {
108109
toolCallIds.push(normalizedId)
109110

110111
const cachedData = this.toolParametersCache.get(part.callID) || this.toolParametersCache.get(normalizedId)
111-
const parameters = cachedData?.parameters || part.parameters
112+
const parameters = cachedData?.parameters ?? part.state?.input ?? part.parameters
112113

113114
toolMetadata.set(normalizedId, {
114115
tool: part.tool,
@@ -668,6 +669,7 @@ export class Janitor {
668669
const missingTools = llmPrunedIds.filter(id => {
669670
const normalizedId = id.toLowerCase()
670671
const metadata = toolMetadata.get(normalizedId)
672+
if (metadata?.tool === 'batch') return false
671673
return !metadata || !foundToolNames.has(metadata.tool)
672674
})
673675

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://json.schemastore.org/package.json",
33
"name": "@tarquinen/opencode-dcp",
4-
"version": "0.3.17",
4+
"version": "0.3.18",
55
"type": "module",
66
"description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context",
77
"main": "./dist/index.js",

0 commit comments

Comments
 (0)