Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 7 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,28 @@ Automatically reduces token usage in OpenCode by removing obsolete tool outputs

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

## Pruning Strategies

DCP implements two complementary strategies:

**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.

**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.

## Installation

Add to your OpenCode config:

```jsonc
// opencode.jsonc
{
"plugins": ["@tarquinen/[email protected].17"]
"plugins": ["@tarquinen/[email protected].18"]
}
```

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

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

> **Note:** Project `plugin` arrays override global completely—include all desired plugins in project config if using both.
## Pruning Strategies

DCP implements two complementary strategies:

**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.

**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.
## How It Works

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.
Expand Down
75 changes: 2 additions & 73 deletions lib/deduplicator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { extractParameterKey } from "./display-utils"

export interface DuplicateDetectionResult {
duplicateIds: string[] // IDs to prune (older duplicates)
deduplicationDetails: Map<string, {
Expand Down Expand Up @@ -85,76 +87,3 @@ function sortObjectKeys(obj: any): any {
}
return sorted
}

export function extractParameterKey(metadata: { tool: string, parameters?: any }): string {
if (!metadata.parameters) return ''

const { tool, parameters } = metadata

if (tool === "read" && parameters.filePath) {
return parameters.filePath
}
if (tool === "write" && parameters.filePath) {
return parameters.filePath
}
if (tool === "edit" && parameters.filePath) {
return parameters.filePath
}

if (tool === "list") {
return parameters.path || '(current directory)'
}
if (tool === "glob") {
if (parameters.pattern) {
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
return `"${parameters.pattern}"${pathInfo}`
}
return '(unknown pattern)'
}
if (tool === "grep") {
if (parameters.pattern) {
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
return `"${parameters.pattern}"${pathInfo}`
}
return '(unknown pattern)'
}

if (tool === "bash") {
if (parameters.description) return parameters.description
if (parameters.command) {
return parameters.command.length > 50
? parameters.command.substring(0, 50) + "..."
: parameters.command
}
}

if (tool === "webfetch" && parameters.url) {
return parameters.url
}
if (tool === "websearch" && parameters.query) {
return `"${parameters.query}"`
}
if (tool === "codesearch" && parameters.query) {
return `"${parameters.query}"`
}

if (tool === "todowrite") {
return `${parameters.todos?.length || 0} todos`
}
if (tool === "todoread") {
return "read todo list"
}

if (tool === "task" && parameters.description) {
return parameters.description
}
if (tool === "batch") {
return `${parameters.tool_calls?.length || 0} parallel tools`
}

const paramStr = JSON.stringify(parameters)
if (paramStr === '{}' || paramStr === '[]' || paramStr === 'null') {
return ''
}
return paramStr.substring(0, 50)
}
76 changes: 76 additions & 0 deletions lib/display-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/**
* Extracts a human-readable key from tool metadata for display purposes.
* Used by both deduplication and AI analysis to show what was pruned.
*/
export function extractParameterKey(metadata: { tool: string, parameters?: any }): string {
if (!metadata.parameters) return ''

const { tool, parameters } = metadata

if (tool === "read" && parameters.filePath) {
return parameters.filePath
}
if (tool === "write" && parameters.filePath) {
return parameters.filePath
}
if (tool === "edit" && parameters.filePath) {
return parameters.filePath
}

if (tool === "list") {
return parameters.path || '(current directory)'
}
if (tool === "glob") {
if (parameters.pattern) {
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
return `"${parameters.pattern}"${pathInfo}`
}
return '(unknown pattern)'
}
if (tool === "grep") {
if (parameters.pattern) {
const pathInfo = parameters.path ? ` in ${parameters.path}` : ""
return `"${parameters.pattern}"${pathInfo}`
}
return '(unknown pattern)'
}

if (tool === "bash") {
if (parameters.description) return parameters.description
if (parameters.command) {
return parameters.command.length > 50
? parameters.command.substring(0, 50) + "..."
: parameters.command
}
}

if (tool === "webfetch" && parameters.url) {
return parameters.url
}
if (tool === "websearch" && parameters.query) {
return `"${parameters.query}"`
}
if (tool === "codesearch" && parameters.query) {
return `"${parameters.query}"`
}

if (tool === "todowrite") {
return `${parameters.todos?.length || 0} todos`
}
if (tool === "todoread") {
return "read todo list"
}

if (tool === "task" && parameters.description) {
return parameters.description
}
if (tool === "batch") {
return `${parameters.tool_calls?.length || 0} parallel tools`
}

const paramStr = JSON.stringify(parameters)
if (paramStr === '{}' || paramStr === '[]' || paramStr === 'null') {
return ''
}
return paramStr.substring(0, 50)
}
6 changes: 4 additions & 2 deletions lib/janitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type { PruningStrategy } from "./config"
import { buildAnalysisPrompt } from "./prompt"
import { selectModel, extractModelFromSession } from "./model-selector"
import { estimateTokensBatch, formatTokenCount } from "./tokenizer"
import { detectDuplicates, extractParameterKey } from "./deduplicator"
import { detectDuplicates } from "./deduplicator"
import { extractParameterKey } from "./display-utils"

export interface SessionStats {
totalToolsPruned: number
Expand Down Expand Up @@ -108,7 +109,7 @@ export class Janitor {
toolCallIds.push(normalizedId)

const cachedData = this.toolParametersCache.get(part.callID) || this.toolParametersCache.get(normalizedId)
const parameters = cachedData?.parameters || part.parameters
const parameters = cachedData?.parameters ?? part.state?.input ?? part.parameters

toolMetadata.set(normalizedId, {
tool: part.tool,
Expand Down Expand Up @@ -668,6 +669,7 @@ export class Janitor {
const missingTools = llmPrunedIds.filter(id => {
const normalizedId = id.toLowerCase()
const metadata = toolMetadata.get(normalizedId)
if (metadata?.tool === 'batch') return false
return !metadata || !foundToolNames.has(metadata.tool)
})

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@tarquinen/opencode-dcp",
"version": "0.3.17",
"version": "0.3.18",
"type": "module",
"description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context",
"main": "./dist/index.js",
Expand Down