diff --git a/README.md b/README.md index 72e4a15..d0907ca 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,6 @@ 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: @@ -21,7 +13,7 @@ Add to your OpenCode config: ```jsonc // opencode.jsonc { - "plugins": ["@tarquinen/opencode-dcp@0.3.17"] + "plugins": ["@tarquinen/opencode-dcp@0.3.18"] } ``` @@ -29,8 +21,13 @@ When a new version is available, DCP will show a toast notification. Update by c 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. diff --git a/lib/deduplicator.ts b/lib/deduplicator.ts index 2ba2c7d..1d64940 100644 --- a/lib/deduplicator.ts +++ b/lib/deduplicator.ts @@ -1,3 +1,5 @@ +import { extractParameterKey } from "./display-utils" + export interface DuplicateDetectionResult { duplicateIds: string[] // IDs to prune (older duplicates) deduplicationDetails: Map 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) -} diff --git a/lib/display-utils.ts b/lib/display-utils.ts new file mode 100644 index 0000000..8006830 --- /dev/null +++ b/lib/display-utils.ts @@ -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) +} diff --git a/lib/janitor.ts b/lib/janitor.ts index 7013f84..738802b 100644 --- a/lib/janitor.ts +++ b/lib/janitor.ts @@ -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 @@ -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, @@ -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) }) diff --git a/package-lock.json b/package-lock.json index 86a6bc1..0d58fa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tarquinen/opencode-dcp", - "version": "0.3.17", + "version": "0.3.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tarquinen/opencode-dcp", - "version": "0.3.17", + "version": "0.3.18", "license": "MIT", "dependencies": { "@ai-sdk/openai-compatible": "^1.0.27", diff --git a/package.json b/package.json index d101121..857cd24 100644 --- a/package.json +++ b/package.json @@ -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",