Skip to content

Commit bcc2509

Browse files
authored
Merge pull request #118 from Opencode-DCP/dev
Merge dev into master
2 parents 42f3116 + 6693dd4 commit bcc2509

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+1749
-3122
lines changed

.claude/settings.local.json

Lines changed: 0 additions & 15 deletions
This file was deleted.

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

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ Thumbs.db
2727

2828
# OpenCode
2929
.opencode/
30-
AGENTS.md
3130

3231
# Tests (local development only)
3332
tests/

README.md

Lines changed: 61 additions & 31 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+
**On Idle Analysis** — Uses a language model to semantically analyze conversation context during idle periods and identify tool outputs that are no longer relevant.
3131

32-
## Context Pruning Tool
32+
**Prune Tool** — Exposes a `prune` tool that the AI can call to manually trigger pruning when it determines context cleanup is needed.
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 `nudge_freq` to control how aggressively the AI is prompted to prune — lower values trigger reminders sooner and more often.
37-
38-
## How It Works
34+
*More strategies coming soon.*
3935

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

@@ -47,40 +43,74 @@ LLM providers like Anthropic and OpenAI cache prompts based on exact prefix matc
4743

4844
## Configuration
4945

50-
DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.jsonc`), created automatically on first run.
46+
DCP uses its own config file:
5147

52-
### Options
48+
- Global: `~/.config/opencode/dcp.jsonc` (or `dcp.json`), created automatically on first run
49+
- Custom config directory: `$OPENCODE_CONFIG_DIR/dcp.jsonc` (or `dcp.json`), if `OPENCODE_CONFIG_DIR` is set
50+
- Project: `.opencode/dcp.jsonc` (or `dcp.json`) in your project’s `.opencode` directory
5351

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-
| `pruning_summary` | `"detailed"` | `"off"`, `"minimal"`, or `"detailed"` |
63-
| `nudge_freq` | `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.
52+
<details>
53+
<summary><strong>Default Configuration</strong> (click to expand)</summary>
6954

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

83-
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.
111+
Settings are merged in order:
112+
Defaults → Global (`~/.config/opencode/dcp.jsonc`) → Config Dir (`$OPENCODE_CONFIG_DIR/dcp.jsonc`) → Project (`.opencode/dcp.jsonc`).
113+
Each level overrides the previous, so project settings take priority over config-dir and global, which take priority over defaults.
84114

85115
Restart OpenCode after making config changes.
86116

index.ts

Lines changed: 29 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import type { Plugin } from "@opencode-ai/plugin"
22
import { getConfig } from "./lib/config"
33
import { Logger } from "./lib/logger"
4-
import { createJanitorContext } from "./lib/core/janitor"
5-
import { checkForUpdates } from "./lib/version-checker"
6-
import { createPluginState } from "./lib/state"
7-
import { installFetchWrapper } from "./lib/fetch-wrapper"
8-
import { createPruningTool } from "./lib/pruning-tool"
9-
import { createEventHandler, createChatParamsHandler } from "./lib/hooks"
10-
import { createToolTracker } from "./lib/fetch-wrapper/tool-tracker"
4+
import { loadPrompt } from "./lib/prompt"
5+
import { createSessionState } from "./lib/state"
6+
import { createPruneTool } from "./lib/strategies"
7+
import { createChatMessageTransformHandler, createEventHandler } from "./lib/hooks"
118

129
const plugin: Plugin = (async (ctx) => {
13-
const { config, migrations } = getConfig(ctx)
10+
const config = getConfig(ctx)
1411

1512
if (!config.enabled) {
1613
return {}
@@ -23,82 +20,46 @@ const plugin: Plugin = (async (ctx) => {
2320

2421
// Initialize core components
2522
const logger = new Logger(config.debug)
26-
const state = createPluginState()
27-
28-
const janitorCtx = createJanitorContext(
29-
ctx.client,
30-
state,
31-
logger,
32-
{
33-
protectedTools: config.protectedTools,
34-
model: config.model,
35-
showModelErrorToasts: config.showModelErrorToasts ?? true,
36-
strictModelSelection: config.strictModelSelection ?? false,
37-
pruningSummary: config.pruning_summary,
38-
workingDirectory: ctx.directory
39-
}
40-
)
41-
42-
// Create tool tracker for nudge injection
43-
const toolTracker = createToolTracker()
44-
45-
// Install global fetch wrapper for context pruning and system message injection
46-
installFetchWrapper(state, logger, ctx.client, config, toolTracker)
23+
const state = createSessionState()
4724

4825
// Log initialization
49-
logger.info("plugin", "DCP initialized", {
26+
logger.info("DCP initialized", {
5027
strategies: config.strategies,
51-
model: config.model || "auto"
5228
})
5329

54-
// Check for updates after a delay
55-
setTimeout(() => {
56-
checkForUpdates(ctx.client, logger, config.showUpdateToasts ?? true).catch(() => { })
57-
}, 5000)
58-
59-
// Show migration toast if there were config migrations
60-
if (migrations.length > 0) {
61-
setTimeout(async () => {
62-
try {
63-
await ctx.client.tui.showToast({
64-
body: {
65-
title: "DCP: Config upgraded",
66-
message: migrations.join('\n'),
67-
variant: "info",
68-
duration: 8000
69-
}
70-
})
71-
} catch {
72-
// Silently ignore toast errors
73-
}
74-
}, 7000)
75-
}
76-
7730
return {
31+
"experimental.chat.system.transform": async (_input: unknown, output: { system: string[] }) => {
32+
const syntheticPrompt = loadPrompt("synthetic")
33+
output.system.push(syntheticPrompt)
34+
},
35+
"experimental.chat.messages.transform": createChatMessageTransformHandler(
36+
ctx.client,
37+
state,
38+
logger,
39+
config
40+
),
41+
tool: config.strategies.pruneTool.enabled ? {
42+
prune: createPruneTool({
43+
client: ctx.client,
44+
state,
45+
logger,
46+
config,
47+
workingDirectory: ctx.directory
48+
}),
49+
} : undefined,
7850
config: async (opencodeConfig) => {
7951
// Add prune to primary_tools by mutating the opencode config
8052
// This works because config is cached and passed by reference
81-
if (config.strategies.onTool.length > 0) {
53+
if (config.strategies.pruneTool.enabled) {
8254
const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? []
8355
opencodeConfig.experimental = {
8456
...opencodeConfig.experimental,
8557
primary_tools: [...existingPrimaryTools, "prune"],
8658
}
87-
logger.info("plugin", "Added 'prune' to experimental.primary_tools via config mutation")
59+
logger.info("Added 'prune' to experimental.primary_tools via config mutation")
8860
}
8961
},
90-
event: createEventHandler(ctx.client, janitorCtx, logger, config, toolTracker),
91-
"chat.params": createChatParamsHandler(ctx.client, state, logger, toolTracker),
92-
tool: config.strategies.onTool.length > 0 ? {
93-
prune: createPruningTool({
94-
client: ctx.client,
95-
state,
96-
logger,
97-
config,
98-
notificationCtx: janitorCtx.notificationCtx,
99-
workingDirectory: ctx.directory
100-
}, toolTracker),
101-
} : undefined,
62+
event: createEventHandler(ctx.client, config, state, logger, ctx.directory),
10263
}
10364
}) satisfies Plugin
10465

0 commit comments

Comments
 (0)