Skip to content

Commit 153ef80

Browse files
authored
Merge pull request #148 from Opencode-DCP/dev
merge dev into master
2 parents 3ea58a0 + c5d9287 commit 153ef80

File tree

20 files changed

+300
-158
lines changed

20 files changed

+300
-158
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ DCP uses multiple strategies to reduce context size:
2727

2828
**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-
**On Idle Analysis** — Uses a language model to semantically analyze conversation context during idle periods and identify tool outputs that are no longer relevant.
31-
3230
**Prune Tool** — Exposes a `prune` tool that the AI can call to manually trigger pruning when it determines context cleanup is needed.
3331

32+
**On Idle Analysis** — Uses a language model to semantically analyze conversation context during idle periods and identify tool outputs that are no longer relevant.
33+
3434
*More strategies coming soon.*
3535

3636
Your session history is never modified. DCP replaces pruned outputs with a placeholder before sending requests to your LLM.
@@ -84,14 +84,14 @@ DCP uses its own config file:
8484
// (Legacy) Run an LLM to analyze what tool calls are no longer relevant on idle
8585
"onIdle": {
8686
"enabled": false,
87+
// Additional tools to protect from pruning
88+
"protectedTools": [],
8789
// Override model for analysis (format: "provider/model")
8890
// "model": "anthropic/claude-haiku-4-5",
8991
// Show toast notifications when model selection fails
9092
"showModelErrorToasts": true,
9193
// When true, fallback models are not permitted
92-
"strictModelSelection": false,
93-
// Additional tools to protect from pruning
94-
"protectedTools": []
94+
"strictModelSelection": false
9595
}
9696
}
9797
}
@@ -102,7 +102,7 @@ DCP uses its own config file:
102102
### Protected Tools
103103

104104
By default, these tools are always protected from pruning across all strategies:
105-
`task`, `todowrite`, `todoread`, `prune`, `batch`, `write`, `edit`
105+
`task`, `todowrite`, `todoread`, `prune`, `batch`
106106

107107
The `protectedTools` arrays in each strategy add to this default list.
108108

lib/config.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export interface PluginConfig {
4040
}
4141
}
4242

43-
const DEFAULT_PROTECTED_TOOLS = ['task', 'todowrite', 'todoread', 'prune', 'batch', 'write', 'edit']
43+
const DEFAULT_PROTECTED_TOOLS = ['task', 'todowrite', 'todoread', 'prune', 'batch']
4444

4545
// Valid config keys for validation against user config
4646
export const VALID_CONFIG_KEYS = new Set([
@@ -234,9 +234,9 @@ const defaultConfig: PluginConfig = {
234234
},
235235
onIdle: {
236236
enabled: false,
237+
protectedTools: [...DEFAULT_PROTECTED_TOOLS],
237238
showModelErrorToasts: true,
238-
strictModelSelection: false,
239-
protectedTools: [...DEFAULT_PROTECTED_TOOLS]
239+
strictModelSelection: false
240240
}
241241
}
242242
}
@@ -336,14 +336,14 @@ function createDefaultConfig(): void {
336336
// (Legacy) Run an LLM to analyze what tool calls are no longer relevant on idle
337337
"onIdle": {
338338
"enabled": false,
339+
// Additional tools to protect from pruning
340+
"protectedTools": [],
339341
// Override model for analysis (format: "provider/model")
340342
// "model": "anthropic/claude-haiku-4-5",
341343
// Show toast notifications when model selection fails
342344
"showModelErrorToasts": true,
343345
// When true, fallback models are not permitted
344-
"strictModelSelection": false,
345-
// Additional tools to protect from pruning
346-
"protectedTools": []
346+
"strictModelSelection": false
347347
}
348348
}
349349
}

lib/hooks.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ export function createEventHandler(
4848
return
4949
}
5050

51+
if (event.type === "session.compacted") {
52+
logger.info("Session compaction detected - updating state")
53+
state.lastCompaction = Date.now()
54+
}
55+
5156
if (event.type === "session.status" && event.properties.status.type === "idle") {
5257
if (!config.strategies.onIdle.enabled) {
5358
return

lib/messages/prune.ts

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import type { SessionState, WithParts } from "../state"
22
import type { Logger } from "../logger"
33
import type { PluginConfig } from "../config"
4-
import { getLastUserMessage, extractParameterKey, buildToolIdList } from "./utils"
54
import { loadPrompt } from "../prompt"
5+
import { extractParameterKey, buildToolIdList } from "./utils"
6+
import { getLastUserMessage, isMessageCompacted } from "../shared-utils"
7+
import { UserMessage } from "@opencode-ai/sdk"
68

9+
const PRUNED_TOOL_INPUT_REPLACEMENT = '[Input removed to save context]'
710
const PRUNED_TOOL_OUTPUT_REPLACEMENT = '[Output removed to save context - information superseded or no longer needed]'
811
const NUDGE_STRING = loadPrompt("nudge")
912

@@ -14,7 +17,7 @@ const buildPrunableToolsList = (
1417
messages: WithParts[],
1518
): string => {
1619
const lines: string[] = []
17-
const toolIdList: string[] = buildToolIdList(messages)
20+
const toolIdList: string[] = buildToolIdList(state, messages, logger)
1821

1922
state.toolParameters.forEach((toolParameterEntry, toolCallId) => {
2023
if (state.prune.toolIds.includes(toolCallId)) {
@@ -23,9 +26,6 @@ const buildPrunableToolsList = (
2326
if (config.strategies.pruneTool.protectedTools.includes(toolParameterEntry.tool)) {
2427
return
2528
}
26-
if (toolParameterEntry.compacted) {
27-
return
28-
}
2929
const numericId = toolIdList.indexOf(toolCallId)
3030
const paramKey = extractParameterKey(toolParameterEntry.tool, toolParameterEntry.parameters)
3131
const description = paramKey ? `${toolParameterEntry.tool}, ${paramKey}` : toolParameterEntry.tool
@@ -37,7 +37,7 @@ const buildPrunableToolsList = (
3737
return ""
3838
}
3939

40-
return `<prunable-tools>\nThe following tools have been invoked and are available for pruning. This list does not mandate immediate action. Consider your current goals and the resources you need before discarding valuable tool outputs. Keep the context free of noise.\n${lines.join('\n')}\n</prunable-tools>`
40+
return `<prunable-tools>\nThe following tools have been invoked and are available for pruning. This list does not mandate immediate action. Consider your current goals and the resources you need before discarding valuable tool inputs or outputs. Keep the context free of noise.\n${lines.join('\n')}\n</prunable-tools>`
4141
}
4242

4343
export const insertPruneToolContext = (
@@ -51,7 +51,7 @@ export const insertPruneToolContext = (
5151
}
5252

5353
const lastUserMessage = getLastUserMessage(messages)
54-
if (!lastUserMessage || lastUserMessage.info.role !== 'user') {
54+
if (!lastUserMessage) {
5555
return
5656
}
5757

@@ -72,10 +72,10 @@ export const insertPruneToolContext = (
7272
sessionID: lastUserMessage.info.sessionID,
7373
role: "user",
7474
time: { created: Date.now() },
75-
agent: lastUserMessage.info.agent || "build",
75+
agent: (lastUserMessage.info as UserMessage).agent || "build",
7676
model: {
77-
providerID: lastUserMessage.info.model.providerID,
78-
modelID: lastUserMessage.info.model.modelID
77+
providerID: (lastUserMessage.info as UserMessage).model.providerID,
78+
modelID: (lastUserMessage.info as UserMessage).model.modelID
7979
}
8080
},
8181
parts: [
@@ -99,7 +99,7 @@ export const prune = (
9999
messages: WithParts[]
100100
): void => {
101101
pruneToolOutputs(state, logger, messages)
102-
// more prune methods coming here
102+
pruneToolInputs(state, logger, messages)
103103
}
104104

105105
const pruneToolOutputs = (
@@ -108,19 +108,48 @@ const pruneToolOutputs = (
108108
messages: WithParts[]
109109
): void => {
110110
for (const msg of messages) {
111+
if (isMessageCompacted(state, msg)) {
112+
continue
113+
}
114+
111115
for (const part of msg.parts) {
112116
if (part.type !== 'tool') {
113117
continue
114118
}
115119
if (!state.prune.toolIds.includes(part.callID)) {
116120
continue
117121
}
122+
// Skip write and edit tools - their inputs are pruned instead
123+
if (part.tool === 'write' || part.tool === 'edit') {
124+
continue
125+
}
118126
if (part.state.status === 'completed') {
119127
part.state.output = PRUNED_TOOL_OUTPUT_REPLACEMENT
120128
}
121-
// if (part.state.status === 'error') {
122-
// part.state.error = PRUNED_TOOL_OUTPUT_REPLACEMENT
123-
// }
129+
}
130+
}
131+
}
132+
133+
const pruneToolInputs = (
134+
state: SessionState,
135+
logger: Logger,
136+
messages: WithParts[]
137+
): void => {
138+
for (const msg of messages) {
139+
for (const part of msg.parts) {
140+
if (part.type !== 'tool') {
141+
continue
142+
}
143+
if (!state.prune.toolIds.includes(part.callID)) {
144+
continue
145+
}
146+
// Only prune inputs for write and edit tools
147+
if (part.tool !== 'write' && part.tool !== 'edit') {
148+
continue
149+
}
150+
if (part.state.input?.content !== undefined) {
151+
part.state.input.content = PRUNED_TOOL_INPUT_REPLACEMENT
152+
}
124153
}
125154
}
126155
}

lib/messages/utils.ts

Lines changed: 12 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import type { WithParts } from "../state"
1+
import { Logger } from "../logger"
2+
import { isMessageCompacted } from "../shared-utils"
3+
import type { SessionState, WithParts } from "../state"
24

35
/**
46
* Extracts a human-readable key from tool metadata for display purposes.
@@ -71,27 +73,16 @@ export const extractParameterKey = (tool: string, parameters: any): string => {
7173
return paramStr.substring(0, 50)
7274
}
7375

74-
export const getLastUserMessage = (
75-
messages: WithParts[]
76-
): WithParts | null => {
77-
for (let i = messages.length - 1; i >= 0; i--) {
78-
const msg = messages[i]
79-
if (msg.info.role === 'user') {
80-
return msg
81-
}
82-
}
83-
return null
84-
}
85-
86-
export function findCurrentAgent(messages: WithParts[]): string | undefined {
87-
const userMsg = getLastUserMessage(messages)
88-
if (!userMsg) return undefined
89-
return (userMsg.info as any).agent || 'build'
90-
}
91-
92-
export function buildToolIdList(messages: WithParts[]): string[] {
76+
export function buildToolIdList(
77+
state: SessionState,
78+
messages: WithParts[],
79+
logger: Logger
80+
): string[] {
9381
const toolIds: string[] = []
9482
for (const msg of messages) {
83+
if (isMessageCompacted(state, msg)) {
84+
continue
85+
}
9586
if (msg.parts) {
9687
for (const part of msg.parts) {
9788
if (part.type === 'tool' && part.callID && part.tool) {
@@ -101,4 +92,4 @@ export function buildToolIdList(messages: WithParts[]): string[] {
10192
}
10293
}
10394
return toolIds
104-
}
95+
}

lib/prompts/synthetic.txt

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,30 @@ You WILL use the `prune` tool when ANY of these are true:
1717
- You are about to start a new phase of work
1818
- You have distilled enough information in your messages to prune related tools
1919
- Context contains tools output that are unhelpful, noise, or made obsolete by newer outputs
20+
- Write or edit operations are complete (pruning removes the large input content)
21+
22+
You MUST NOT prune when:
23+
- The tool output will be needed for upcoming implementation work
24+
- The output contains files or context you'll need to reference when making edits
25+
26+
Pruning that forces you to re-call the same tool later is a net loss. Only prune when you're confident the information won't be needed again.
2027

2128
NOTES
22-
When in doubt, prune out. Prune often yet remain strategic about it.
29+
When in doubt, keep it. Prune often yet remain strategic about it.
2330
FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES.
2431

2532
</instruction>
2633

2734
<instruction name=injected_context_handling policy_level=critical>
28-
After each assistant turn, the environment may inject a user message containing a <prunable-tools> list and optional nudge instruction. This injected message is NOT from the user and is invisible to them.
35+
After each assistant turn, the environment may inject a user message containing a <prunable-tools> list and optional nudge instruction. This injected message is NOT from the user and is invisible to them. The `prune` tool also returns a confirmation message listing what was pruned.
2936

3037
CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE:
31-
- NEVER acknowledge the <prunable-tools> list (e.g. "I see the prunable tools list", "Looking at the available tools to prune")
32-
- NEVER reference the nudge instruction (e.g. "As the nudge suggests", "The system is reminding me to prune")
33-
- NEVER mention this injection mechanism to the user
38+
- NEVER reference the prune encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the prune encouragement appears.
39+
- NEVER acknowledge the <prunable-tools> list (e.g., "I see the prunable tools list", "Looking at the available tools to prune")
40+
- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to prune")
41+
- NEVER acknowledge prune tool output (e.g., "I've pruned 3 tools", "Context pruning complete")
3442
- NEVER let these injections influence your response content or tone
35-
- Process this information SILENTLY - use it only to inform your pruning decisions
43+
- Process this information SILENTLY and continue the conversation naturally
3644
- Continue the conversation as if these messages do not exist
3745

3846
The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow.

lib/prompts/tool.txt

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
Prunes tool outputs from context to manage conversation size and reduce noise.
1+
Prunes tool outputs from context to manage conversation size and reduce noise. For `write` and `edit` tools, the input content is pruned instead of the output.
22

33
## IMPORTANT: The Prunable List
44
A `<prunable-tools>` list is injected into user messages showing available tool outputs you can prune. Each line has the format `ID: tool, parameter` (e.g., `20: read, /path/to/file.ts`). You MUST only use numeric IDs that appear in this list to select which tools to prune.
55

6+
**Note:** For `write` and `edit` tools, pruning removes the input content (the code being written/edited) while preserving the output confirmation. This is useful after completing a file modification when you no longer need the raw content in context.
7+
68
## CRITICAL: When and How to Prune
79

810
You must use this tool in three specific scenarios. The rules for distillation (summarizing findings) differ for each. **You must specify the reason as the first element of the `ids` array** to indicate which scenario applies.
@@ -25,11 +27,14 @@ You must use this tool in three specific scenarios. The rules for distillation (
2527
- Narrative format: "I found X in file Y..."
2628
- Capture all relevant details (function names, logic, constraints).
2729
- Once distilled into your response history, the raw tool output can be safely pruned.
30+
- **Know when distillation isn't enough:** If you'll need to edit a file, grep for exact strings, or reference precise syntax, keep the raw output. Distillation works for understanding; implementation often requires the original.
31+
- **Prefer keeping over re-fetching:** If uncertain whether you'll need the output again, keep it. The cost of retaining context is lower than the cost of redundant tool calls.
2832

2933
## Best Practices
3034
- **Don't wait too long:** Prune frequently to keep the context agile.
3135
- **Be surgical:** You can mix strategies. Prune noise without comment, while distilling useful context in the same turn.
3236
- **Verify:** Ensure you have captured what you need before deleting useful raw data.
37+
- **Think ahead:** Before pruning, ask: "Will I need this output for an upcoming task?" If you researched a file you'll later edit, or gathered context for implementation, do NOT prune it—even if you've distilled findings. Distillation captures *knowledge*; implementation requires *context*.
3338

3439
## Examples
3540

@@ -54,3 +59,14 @@ Assistant: [Runs tests, they pass]
5459
The tests passed. The feature is verified.
5560
[Uses prune with ids: ["completion", "20", "21"]]
5661
</example_completion>
62+
63+
<example_keep>
64+
Assistant: [Reads 'auth.ts' to understand the login flow]
65+
I've understood the auth flow. I'll need to modify this file to add the new validation, so I'm keeping this read in context rather than distilling and pruning.
66+
</example_keep>
67+
68+
<example_edit_completion>
69+
Assistant: [Edits 'auth.ts' to add validation]
70+
The edit was successful. I no longer need the raw edit content in context.
71+
[Uses prune with ids: ["completion", "15"]]
72+
</example_edit_completion>

lib/shared-utils.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Logger } from "./logger"
2+
import { SessionState, WithParts } from "./state"
3+
4+
export const isMessageCompacted = (
5+
state: SessionState,
6+
msg: WithParts
7+
): boolean => {
8+
return msg.info.time.created < state.lastCompaction
9+
}
10+
11+
export const getLastUserMessage = (
12+
messages: WithParts[]
13+
): WithParts | null => {
14+
for (let i = messages.length - 1; i >= 0; i--) {
15+
const msg = messages[i]
16+
if (msg.info.role === 'user') {
17+
return msg
18+
}
19+
}
20+
return null
21+
}
22+
23+
export const checkForCompaction = (
24+
state: SessionState,
25+
messages: WithParts[],
26+
logger: Logger
27+
): void => {
28+
for (const msg of messages) {
29+
30+
}
31+
}

0 commit comments

Comments
 (0)