Skip to content

Commit 8e60483

Browse files
authored
Merge pull request #308 from Opencode-DCP/dev
merge dev into master
2 parents 1e29a43 + 3d2869d commit 8e60483

File tree

16 files changed

+105
-106
lines changed

16 files changed

+105
-106
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![npm version](https://img.shields.io/npm/v/@tarquinen/opencode-dcp.svg)](https://www.npmjs.com/package/@tarquinen/opencode-dcp)
44

5-
Automatically reduces token usage in OpenCode by removing obsolete tool outputs from conversation history.
5+
Automatically reduces token usage in OpenCode by removing obsolete tools from conversation history.
66

77
![DCP in action](dcp-demo5.png)
88

lib/commands/context.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ function analyzeTokens(state: SessionState, messages: WithParts[]): TokenBreakdo
117117
if (isMessageCompacted(state, msg)) continue
118118
if (msg.info.role === "user" && isIgnoredUserMessage(msg)) continue
119119

120-
for (const part of msg.parts) {
120+
const parts = Array.isArray(msg.parts) ? msg.parts : []
121+
for (const part of parts) {
121122
if (part.type === "text" && msg.info.role === "user") {
122123
const textPart = part as TextPart
123124
const text = textPart.text || ""

lib/commands/sweep.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ function collectToolIdsAfterIndex(
5252
if (isMessageCompacted(state, msg)) {
5353
continue
5454
}
55-
if (msg.parts) {
56-
for (const part of msg.parts) {
55+
const parts = Array.isArray(msg.parts) ? msg.parts : []
56+
if (parts.length > 0) {
57+
for (const part of parts) {
5758
if (part.type === "tool" && part.callID && part.tool) {
5859
toolIds.push(part.callID)
5960
}

lib/config.ts

Lines changed: 1 addition & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -531,69 +531,7 @@ function createDefaultConfig(): void {
531531
}
532532

533533
const configContent = `{
534-
"$schema": "https://raw.githubusercontent.com/Opencode-DCP/opencode-dynamic-context-pruning/master/dcp.schema.json",
535-
// Enable or disable the plugin
536-
"enabled": true,
537-
// Enable debug logging to ~/.config/opencode/logs/dcp/
538-
"debug": false,
539-
// Notification display: "off", "minimal", or "detailed"
540-
"pruneNotification": "detailed",
541-
// Slash commands (/dcp) configuration
542-
"commands": {
543-
"enabled": true,
544-
// Additional tools to protect from pruning via commands
545-
"protectedTools": []
546-
},
547-
// Protect from pruning for <turns> message turns
548-
"turnProtection": {
549-
"enabled": false,
550-
"turns": 4
551-
},
552-
// Protect file operations from pruning via glob patterns
553-
// Patterns match tool parameters.filePath (e.g. read/write/edit)
554-
"protectedFilePatterns": [],
555-
// LLM-driven context pruning tools
556-
"tools": {
557-
// Shared settings for all prune tools
558-
"settings": {
559-
// Nudge the LLM to use prune tools (every <nudgeFrequency> tool results)
560-
"nudgeEnabled": true,
561-
"nudgeFrequency": 10,
562-
// Additional tools to protect from pruning
563-
"protectedTools": []
564-
},
565-
// Removes tool content from context without preservation (for completed tasks or noise)
566-
"discard": {
567-
"enabled": true
568-
},
569-
// Distills key findings into preserved knowledge before removing raw content
570-
"extract": {
571-
"enabled": true,
572-
// Show distillation content as an ignored message notification
573-
"showDistillation": false
574-
}
575-
},
576-
// Automatic pruning strategies
577-
"strategies": {
578-
// Remove duplicate tool calls (same tool with same arguments)
579-
"deduplication": {
580-
"enabled": true,
581-
// Additional tools to protect from pruning
582-
"protectedTools": []
583-
},
584-
// Prune write tool inputs when the file has been subsequently read
585-
"supersedeWrites": {
586-
"enabled": false
587-
},
588-
// Prune tool inputs for errored tools after X turns
589-
"purgeErrors": {
590-
"enabled": true,
591-
// Number of turns before errored tool inputs are pruned
592-
"turns": 4,
593-
// Additional tools to protect from pruning
594-
"protectedTools": []
595-
}
596-
}
534+
"$schema": "https://raw.githubusercontent.com/Opencode-DCP/opencode-dynamic-context-pruning/master/dcp.schema.json"
597535
}
598536
`
599537
writeFileSync(GLOBAL_CONFIG_PATH_JSONC, configContent, "utf-8")

lib/messages/inject.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
extractParameterKey,
88
buildToolIdList,
99
createSyntheticAssistantMessage,
10+
createSyntheticUserMessage,
1011
isIgnoredUserMessage,
1112
} from "./utils"
1213
import { getFilePathFromParameters, isProtectedFilePath } from "../protected-file-patterns"
@@ -138,16 +139,18 @@ export const insertPruneToolContext = (
138139
return
139140
}
140141

141-
// Never inject immediately following a user message - wait until assistant has started its turn
142-
// This avoids interfering with model reasoning/thinking phases
143-
// TODO: This can be skipped if there is a good way to check if the model has reasoning,
144-
// can't find a good way to do this yet
145-
const lastMessage = messages[messages.length - 1]
146-
if (lastMessage?.info?.role === "user" && !isIgnoredUserMessage(lastMessage)) {
147-
return
148-
}
149-
150142
const userInfo = lastUserMessage.info as UserMessage
151143
const variant = state.variant ?? userInfo.variant
152-
messages.push(createSyntheticAssistantMessage(lastUserMessage, prunableToolsContent, variant))
144+
145+
const lastMessage = messages[messages.length - 1]
146+
const isLastMessageUser =
147+
lastMessage?.info?.role === "user" && !isIgnoredUserMessage(lastMessage)
148+
149+
if (isLastMessageUser) {
150+
messages.push(createSyntheticUserMessage(lastUserMessage, prunableToolsContent, variant))
151+
} else {
152+
messages.push(
153+
createSyntheticAssistantMessage(lastUserMessage, prunableToolsContent, variant),
154+
)
155+
}
153156
}

lib/messages/prune.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ const pruneToolOutputs = (state: SessionState, logger: Logger, messages: WithPar
2525
continue
2626
}
2727

28-
for (const part of msg.parts) {
28+
const parts = Array.isArray(msg.parts) ? msg.parts : []
29+
for (const part of parts) {
2930
if (part.type !== "tool") {
3031
continue
3132
}
@@ -50,7 +51,8 @@ const pruneToolInputs = (state: SessionState, logger: Logger, messages: WithPart
5051
continue
5152
}
5253

53-
for (const part of msg.parts) {
54+
const parts = Array.isArray(msg.parts) ? msg.parts : []
55+
for (const part of parts) {
5456
if (part.type !== "tool") {
5557
continue
5658
}
@@ -77,7 +79,8 @@ const pruneToolErrors = (state: SessionState, logger: Logger, messages: WithPart
7779
continue
7880
}
7981

80-
for (const part of msg.parts) {
82+
const parts = Array.isArray(msg.parts) ? msg.parts : []
83+
for (const part of parts) {
8184
if (part.type !== "tool") {
8285
continue
8386
}

lib/messages/utils.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,36 @@ const isGeminiModel = (modelID: string): boolean => {
1212
return lowerModelID.includes("gemini")
1313
}
1414

15+
export const createSyntheticUserMessage = (
16+
baseMessage: WithParts,
17+
content: string,
18+
variant?: string,
19+
): WithParts => {
20+
const userInfo = baseMessage.info as UserMessage
21+
const now = Date.now()
22+
23+
return {
24+
info: {
25+
id: SYNTHETIC_MESSAGE_ID,
26+
sessionID: userInfo.sessionID,
27+
role: "user" as const,
28+
agent: userInfo.agent || "code",
29+
model: userInfo.model,
30+
time: { created: now },
31+
...(variant !== undefined && { variant }),
32+
},
33+
parts: [
34+
{
35+
id: SYNTHETIC_PART_ID,
36+
sessionID: userInfo.sessionID,
37+
messageID: SYNTHETIC_MESSAGE_ID,
38+
type: "text",
39+
text: content,
40+
},
41+
],
42+
}
43+
}
44+
1545
export const createSyntheticAssistantMessage = (
1646
baseMessage: WithParts,
1747
content: string,
@@ -197,8 +227,9 @@ export function buildToolIdList(
197227
if (isMessageCompacted(state, msg)) {
198228
continue
199229
}
200-
if (msg.parts) {
201-
for (const part of msg.parts) {
230+
const parts = Array.isArray(msg.parts) ? msg.parts : []
231+
if (parts.length > 0) {
232+
for (const part of parts) {
202233
if (part.type === "tool" && part.callID && part.tool) {
203234
toolIds.push(part.callID)
204235
}
@@ -209,11 +240,12 @@ export function buildToolIdList(
209240
}
210241

211242
export const isIgnoredUserMessage = (message: WithParts): boolean => {
212-
if (!message.parts || message.parts.length === 0) {
243+
const parts = Array.isArray(message.parts) ? message.parts : []
244+
if (parts.length === 0) {
213245
return true
214246
}
215247

216-
for (const part of message.parts) {
248+
for (const part of parts) {
217249
if (!(part as any).ignored) {
218250
return false
219251
}

lib/prompts/discard-tool-spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Use \`discard\` for removing tool content that is no longer needed
1212
1313
## When NOT to Use This Tool
1414
15-
- **If the output contains useful information:** Use \`extract\` instead to preserve key findings.
15+
- **If the output contains useful information:** Keep it in context rather than discarding.
1616
- **If you'll need the output later:** Don't discard files you plan to edit or context you'll need for implementation.
1717
1818
## Best Practices

lib/prompts/system/both.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const SYSTEM_PROMPT_BOTH = `<system-reminder>
22
<instruction name=context_management_protocol policy_level=critical>
33
44
ENVIRONMENT
5-
You are operating in a context-constrained environment and thus must proactively manage your context window using the \`discard\` and \`extract\` tools. The environment calls the \`context_info\` tool to provide an up-to-date <prunable-tools> list after each assistant turn. Use this information when deciding what to prune.
5+
You are operating in a context-constrained environment and thus must proactively manage your context window using the \`discard\` and \`extract\` tools. The environment calls the \`context_info\` tool to provide an up-to-date <prunable-tools> list after each turn. Use this information when deciding what to prune.
66
77
IMPORTANT: The \`context_info\` tool is only available to the environment - you do not have access to it and must not attempt to call it.
88
@@ -44,7 +44,7 @@ There may be tools in session context that do not appear in the <prunable-tools>
4444
</instruction>
4545
4646
<instruction name=injected_context_handling policy_level=critical>
47-
After each assistant turn, the environment calls the \`context_info\` tool to inject an assistant message containing a <prunable-tools> list and optional nudge instruction. This tool is only available to the environment - you do not have access to it.
47+
After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a <prunable-tools> list and optional nudge instruction. This tool is only available to the environment - you do not have access to it.
4848
4949
CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE:
5050
- NEVER reference the prune encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the prune encouragement appears.

lib/prompts/system/discard.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ export const SYSTEM_PROMPT_DISCARD = `<system-reminder>
22
<instruction name=context_management_protocol policy_level=critical>
33
44
ENVIRONMENT
5-
You are operating in a context-constrained environment and thus must proactively manage your context window using the \`discard\` tool. The environment calls the \`context_info\` tool to provide an up-to-date <prunable-tools> list after each assistant turn. Use this information when deciding what to discard.
5+
You are operating in a context-constrained environment and thus must proactively manage your context window using the \`discard\` tool. The environment calls the \`context_info\` tool to provide an up-to-date <prunable-tools> list after each turn. Use this information when deciding what to discard.
66
77
IMPORTANT: The \`context_info\` tool is only available to the environment - you do not have access to it and must not attempt to call it.
88
@@ -35,7 +35,7 @@ There may be tools in session context that do not appear in the <prunable-tools>
3535
</instruction>
3636
3737
<instruction name=injected_context_handling policy_level=critical>
38-
After each assistant turn, the environment calls the \`context_info\` tool to inject an assistant message containing a <prunable-tools> list and optional nudge instruction. This tool is only available to the environment - you do not have access to it.
38+
After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a <prunable-tools> list and optional nudge instruction. This tool is only available to the environment - you do not have access to it.
3939
4040
CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE:
4141
- NEVER reference the discard encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the discard encouragement appears.

0 commit comments

Comments
 (0)