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
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ DCP uses its own config file:
"enabled": false,
"turns": 4,
},
// Protect file operations from pruning via glob patterns
// Patterns match tool parameters.filePath (e.g. read/write/edit)
"protectedFilePatterns": [],
// LLM-driven context pruning tools
"tools": {
// Shared settings for all prune tools
Expand Down Expand Up @@ -141,6 +144,10 @@ Each level overrides the previous, so project settings take priority over config

Restart OpenCode after making config changes.

## Limitations

**Subagents** — DCP is disabled for subagents. Subagents are not designed to be token efficient; what matters is that the final message returned to the main agent is a concise summary of findings. DCP's pruning could interfere with this summarization behavior.

## License

MIT
207 changes: 207 additions & 0 deletions dcp.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://raw.githubusercontent.com/Opencode-DCP/opencode-dynamic-context-pruning/main/dcp.schema.json",
"title": "DCP Plugin Configuration",
"description": "Configuration schema for the OpenCode Dynamic Context Pruning plugin",
"type": "object",
"additionalProperties": false,
"properties": {
"$schema": {
"type": "string",
"description": "JSON Schema reference for IDE autocomplete"
},
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable or disable the DCP plugin"
},
"debug": {
"type": "boolean",
"default": false,
"description": "Enable debug logging"
},
"pruneNotification": {
"type": "string",
"enum": ["off", "minimal", "detailed"],
"default": "detailed",
"description": "Level of notification shown when pruning occurs"
},
"turnProtection": {
"type": "object",
"description": "Protect recent tool outputs from being pruned",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": false,
"description": "Enable turn-based protection"
},
"turns": {
"type": "number",
"default": 4,
"description": "Number of recent turns to protect from pruning"
}
}
},
"protectedFilePatterns": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "Glob patterns for files that should be protected from pruning (e.g., '**/*.config.ts')"
},
"tools": {
"type": "object",
"description": "Configuration for pruning tools",
"additionalProperties": false,
"properties": {
"settings": {
"type": "object",
"description": "General tool settings",
"additionalProperties": false,
"properties": {
"nudgeEnabled": {
"type": "boolean",
"default": true,
"description": "Enable nudge reminders to prune context"
},
"nudgeFrequency": {
"type": "number",
"default": 10,
"description": "Frequency of nudge reminders (in turns)"
},
"protectedTools": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"task",
"todowrite",
"todoread",
"discard",
"extract",
"batch",
"write",
"edit"
],
"description": "Tool names that should be protected from automatic pruning"
}
}
},
"discard": {
"type": "object",
"description": "Configuration for the discard tool",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable the discard tool"
}
}
},
"extract": {
"type": "object",
"description": "Configuration for the extract tool",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable the extract tool"
},
"showDistillation": {
"type": "boolean",
"default": false,
"description": "Show distillation output in the UI"
}
}
}
}
},
"strategies": {
"type": "object",
"description": "Automatic pruning strategies",
"additionalProperties": false,
"properties": {
"deduplication": {
"type": "object",
"description": "Remove duplicate tool outputs",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable deduplication strategy"
},
"protectedTools": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"task",
"todowrite",
"todoread",
"discard",
"extract",
"batch",
"write",
"edit"
],
"description": "Tool names excluded from deduplication"
}
}
},
"supersedeWrites": {
"type": "object",
"description": "Replace older write/edit outputs when new ones target the same file",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": false,
"description": "Enable supersede writes strategy"
}
}
},
"purgeErrors": {
"type": "object",
"description": "Remove tool outputs that resulted in errors",
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"default": true,
"description": "Enable purge errors strategy"
},
"turns": {
"type": "number",
"default": 4,
"description": "Number of turns after which errors are purged"
},
"protectedTools": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"task",
"todowrite",
"todoread",
"discard",
"extract",
"batch",
"write",
"edit"
],
"description": "Tool names excluded from error purging"
}
}
}
}
}
}
}
42 changes: 42 additions & 0 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export interface PluginConfig {
debug: boolean
pruneNotification: "off" | "minimal" | "detailed"
turnProtection: TurnProtection
protectedFilePatterns: string[]
tools: Tools
strategies: {
deduplication: Deduplication
Expand Down Expand Up @@ -79,6 +80,7 @@ export const VALID_CONFIG_KEYS = new Set([
"turnProtection",
"turnProtection.enabled",
"turnProtection.turns",
"protectedFilePatterns",
"tools",
"tools.settings",
"tools.settings.nudgeEnabled",
Expand Down Expand Up @@ -151,6 +153,22 @@ function validateConfigTypes(config: Record<string, any>): ValidationError[] {
}
}

if (config.protectedFilePatterns !== undefined) {
if (!Array.isArray(config.protectedFilePatterns)) {
errors.push({
key: "protectedFilePatterns",
expected: "string[]",
actual: typeof config.protectedFilePatterns,
})
} else if (!config.protectedFilePatterns.every((v) => typeof v === "string")) {
errors.push({
key: "protectedFilePatterns",
expected: "string[]",
actual: "non-string entries",
})
}
}

// Top-level turnProtection validator
if (config.turnProtection) {
if (
Expand Down Expand Up @@ -371,6 +389,7 @@ const defaultConfig: PluginConfig = {
enabled: false,
turns: 4,
},
protectedFilePatterns: [],
tools: {
settings: {
nudgeEnabled: true,
Expand Down Expand Up @@ -469,6 +488,7 @@ function createDefaultConfig(): void {
}

const configContent = `{
"$schema": "https://raw.githubusercontent.com/Opencode-DCP/opencode-dynamic-context-pruning/main/dcp.schema.json",
// Enable or disable the plugin
"enabled": true,
// Enable debug logging to ~/.config/opencode/logs/dcp/
Expand All @@ -480,6 +500,9 @@ function createDefaultConfig(): void {
"enabled": false,
"turns": 4
},
// Protect file operations from pruning via glob patterns
// Patterns match tool parameters.filePath (e.g. read/write/edit)
"protectedFilePatterns": [],
// LLM-driven context pruning tools
"tools": {
// Shared settings for all prune tools
Expand Down Expand Up @@ -615,6 +638,7 @@ function deepCloneConfig(config: PluginConfig): PluginConfig {
return {
...config,
turnProtection: { ...config.turnProtection },
protectedFilePatterns: [...config.protectedFilePatterns],
tools: {
settings: {
...config.tools.settings,
Expand Down Expand Up @@ -670,6 +694,12 @@ export function getConfig(ctx: PluginInput): PluginConfig {
enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled,
turns: result.data.turnProtection?.turns ?? config.turnProtection.turns,
},
protectedFilePatterns: [
...new Set([
...config.protectedFilePatterns,
...(result.data.protectedFilePatterns ?? []),
]),
],
tools: mergeTools(config.tools, result.data.tools as any),
strategies: mergeStrategies(config.strategies, result.data.strategies as any),
}
Expand Down Expand Up @@ -706,6 +736,12 @@ export function getConfig(ctx: PluginInput): PluginConfig {
enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled,
turns: result.data.turnProtection?.turns ?? config.turnProtection.turns,
},
protectedFilePatterns: [
...new Set([
...config.protectedFilePatterns,
...(result.data.protectedFilePatterns ?? []),
]),
],
tools: mergeTools(config.tools, result.data.tools as any),
strategies: mergeStrategies(config.strategies, result.data.strategies as any),
}
Expand Down Expand Up @@ -739,6 +775,12 @@ export function getConfig(ctx: PluginInput): PluginConfig {
enabled: result.data.turnProtection?.enabled ?? config.turnProtection.enabled,
turns: result.data.turnProtection?.turns ?? config.turnProtection.turns,
},
protectedFilePatterns: [
...new Set([
...config.protectedFilePatterns,
...(result.data.protectedFilePatterns ?? []),
]),
],
tools: mergeTools(config.tools, result.data.tools as any),
strategies: mergeStrategies(config.strategies, result.data.strategies as any),
}
Expand Down
6 changes: 6 additions & 0 deletions lib/messages/inject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { PluginConfig } from "../config"
import type { UserMessage } from "@opencode-ai/sdk/v2"
import { loadPrompt } from "../prompts"
import { extractParameterKey, buildToolIdList, createSyntheticUserMessage } from "./utils"
import { getFilePathFromParameters, isProtectedFilePath } from "../protected-file-patterns"
import { getLastUserMessage } from "../shared-utils"

const getNudgeString = (config: PluginConfig): string => {
Expand Down Expand Up @@ -62,6 +63,11 @@ const buildPrunableToolsList = (
return
}

const filePath = getFilePathFromParameters(toolParameterEntry.parameters)
if (isProtectedFilePath(filePath, config.protectedFilePatterns)) {
return
}

const numericId = toolIdList.indexOf(toolCallId)
if (numericId === -1) {
logger.warn(`Tool in cache but not in toolIdList - possible stale entry`, {
Expand Down
Loading