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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Add to your OpenCode config:
```jsonc
// opencode.jsonc
{
"plugin": ["@tarquinen/[email protected].27"]
"plugin": ["@tarquinen/[email protected].28"]
}
```

Expand All @@ -31,7 +31,7 @@ DCP implements two complementary strategies:

## Context Pruning Tool

When `strategies.onTool` is enabled, DCP exposes a `context_pruning` tool to Opencode that the AI can call to trigger pruning on demand.
When `strategies.onTool` is enabled, DCP exposes a `prune` tool to Opencode that the AI can call to trigger pruning on demand.

When `nudge_freq` is enabled, injects reminders (every `nudge_freq` tool results) prompting the AI to consider pruning when appropriate.

Expand Down Expand Up @@ -60,9 +60,9 @@ DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.j
| `strictModelSelection` | `false` | Only run AI analysis with session or configured model (disables fallback models) |
| `pruning_summary` | `"detailed"` | `"off"`, `"minimal"`, or `"detailed"` |
| `nudge_freq` | `10` | How often to remind AI to prune (lower = more frequent) |
| `protectedTools` | `["task", "todowrite", "todoread", "context_pruning"]` | Tools that are never pruned |
| `protectedTools` | `["task", "todowrite", "todoread", "prune"]` | Tools that are never pruned |
| `strategies.onIdle` | `["deduplication", "ai-analysis"]` | Strategies for automatic pruning |
| `strategies.onTool` | `["deduplication", "ai-analysis"]` | Strategies when AI calls `context_pruning` |
| `strategies.onTool` | `["deduplication", "ai-analysis"]` | Strategies when AI calls `prune` |

**Strategies:** `"deduplication"` (fast, zero LLM cost) and `"ai-analysis"` (maximum savings). Empty array disables that trigger.

Expand All @@ -73,7 +73,7 @@ DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.j
"onIdle": ["deduplication", "ai-analysis"],
"onTool": ["deduplication", "ai-analysis"]
},
"protectedTools": ["task", "todowrite", "todoread", "context_pruning"]
"protectedTools": ["task", "todowrite", "todoread", "prune"]
}
```

Expand Down
2 changes: 1 addition & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const plugin: Plugin = (async (ctx) => {
event: createEventHandler(ctx.client, janitor, logger, config, toolTracker),
"chat.params": createChatParamsHandler(ctx.client, state, logger),
tool: config.strategies.onTool.length > 0 ? {
prune: createPruningTool(janitor, config, toolTracker),
prune: createPruningTool(ctx.client, janitor, config, toolTracker),
} : undefined,
}
}) satisfies Plugin
Expand Down
2 changes: 1 addition & 1 deletion lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function createDefaultConfig(): void {
"strategies": {
// Strategies to run when session goes idle
"onIdle": ["deduplication", "ai-analysis"],
// Strategies to run when AI calls context_pruning tool
// Strategies to run when AI calls prune tool
"onTool": ["deduplication", "ai-analysis"]
},
// Summary display: "off", "minimal", or "detailed"
Expand Down
4 changes: 2 additions & 2 deletions lib/fetch-wrapper/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export async function handleGemini(
// Inject periodic nudge based on tool result count
if (ctx.config.nudge_freq > 0) {
if (injectNudgeGemini(body.contents, ctx.toolTracker, ctx.prompts.nudgeInstruction, ctx.config.nudge_freq)) {
ctx.logger.info("fetch", "Injected nudge instruction (Gemini)")
// ctx.logger.info("fetch", "Injected nudge instruction (Gemini)")
modified = true
}
}
Expand All @@ -38,7 +38,7 @@ export async function handleGemini(
}

if (injectSynthGemini(body.contents, ctx.prompts.synthInstruction, ctx.prompts.nudgeInstruction)) {
ctx.logger.info("fetch", "Injected synthetic instruction (Gemini)")
// ctx.logger.info("fetch", "Injected synthetic instruction (Gemini)")
modified = true
}
}
Expand Down
4 changes: 2 additions & 2 deletions lib/fetch-wrapper/openai-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function handleOpenAIChatAndAnthropic(
// Inject periodic nudge based on tool result count
if (ctx.config.nudge_freq > 0) {
if (injectNudge(body.messages, ctx.toolTracker, ctx.prompts.nudgeInstruction, ctx.config.nudge_freq)) {
ctx.logger.info("fetch", "Injected nudge instruction")
// ctx.logger.info("fetch", "Injected nudge instruction")
modified = true
}
}
Expand All @@ -43,7 +43,7 @@ export async function handleOpenAIChatAndAnthropic(
}

if (injectSynth(body.messages, ctx.prompts.synthInstruction, ctx.prompts.nudgeInstruction)) {
ctx.logger.info("fetch", "Injected synthetic instruction")
// ctx.logger.info("fetch", "Injected synthetic instruction")
modified = true
}
}
Expand Down
4 changes: 2 additions & 2 deletions lib/fetch-wrapper/openai-responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export async function handleOpenAIResponses(
// Inject periodic nudge based on tool result count
if (ctx.config.nudge_freq > 0) {
if (injectNudgeResponses(body.input, ctx.toolTracker, ctx.prompts.nudgeInstruction, ctx.config.nudge_freq)) {
ctx.logger.info("fetch", "Injected nudge instruction (Responses API)")
// ctx.logger.info("fetch", "Injected nudge instruction (Responses API)")
modified = true
}
}
Expand All @@ -43,7 +43,7 @@ export async function handleOpenAIResponses(
}

if (injectSynthResponses(body.input, ctx.prompts.synthInstruction, ctx.prompts.nudgeInstruction)) {
ctx.logger.info("fetch", "Injected synthetic instruction (Responses API)")
// ctx.logger.info("fetch", "Injected synthetic instruction (Responses API)")
modified = true
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function createEventHandler(
if (await isSubagentSession(client, event.properties.sessionID)) return
if (config.strategies.onIdle.length === 0) return

// Skip idle pruning if the last tool used was context_pruning
// Skip idle pruning if the last tool used was prune
// and idle strategies cover the same work as tool strategies
if (toolTracker?.skipNextIdle) {
toolTracker.skipNextIdle = false
Expand Down
2 changes: 1 addition & 1 deletion lib/prompts/nudge.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ This nudge is injected by a plugin and is invisible to the user. Do not acknowle
</system-reminder>

<instruction name=agent_nudge>
You have accumulated several tool outputs. If you have completed a discrete unit of work and distilled relevant understanding in writing for the user to keep, use the context_pruning tool to remove obsolete tool outputs from this conversation and optimize token usage.
You have accumulated several tool outputs. If you have completed a discrete unit of work and distilled relevant understanding in writing for the user to keep, use the prune tool to remove obsolete tool outputs from this conversation and optimize token usage.
</instruction>
15 changes: 8 additions & 7 deletions lib/prompts/synthetic.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,30 @@ THIS IS NON-NEGOTIABLE - YOU ARE EXPECTED TO RESPECT THIS INSTRUCTION THROUGHOUT
</instruction>

<instruction name=context_window_management>
A strong constraint we have in this environment is the context window size. To help keep the conversation focused and clear from the noise, you must use the `context_pruning` tool: at opportune moments, and in an effective manner.
A strong constraint we have in this environment is the context window size. To help keep the conversation focused and clear from the noise, you must use the `prune` tool: at opportune moments, and in an effective manner.
</instruction>

<instruction name=context_pruning>
To effectively manage conversation context, you MUST ALWAYS narrate your findings AS YOU DISCOVER THEM, BEFORE calling any `context_pruning` tool. No tool result (read, bash, grep, webfetch, etc.) should be left unexplained. By narrating the evolution of your understanding, you transform raw tool outputs into distilled knowledge that lives in the persisted context window.
To effectively manage conversation context, you MUST ALWAYS narrate your findings AS YOU DISCOVER THEM, BEFORE calling any `prune` tool. No tool result (read, bash, grep, webfetch, etc.) should be left unexplained. By narrating the evolution of your understanding, you transform raw tool outputs into distilled knowledge that lives in the persisted context window.

Tools are VOLATILE - Once this distilled knowledge is in your reply, you can safely use the `context_pruning` tool to declutter the conversation.
Tools are VOLATILE - Once this distilled knowledge is in your reply, you can safely use the `prune` tool to declutter the conversation.

WHEN TO USE `context_pruning`:
WHEN TO USE `prune`:
- After you complete a discrete unit of work (e.g. confirming a hypothesis, or closing out one branch of investigation).
- After exploratory bursts of tool calls that led you to a clear conclusion. (or to noise)
- Before starting a new phase of work where old tool outputs are no longer needed to inform your next actions.

CRITICAL:
You must ALWAYS narrate your findings in a message BEFORE using the `context_pruning` tool. Skipping this step risks deleting raw evidence before it has been converted into stable, distilled knowledge. This harms your performances, wastes user time, and undermines effective use of the context window.
You must ALWAYS narrate your findings in a message BEFORE using the `prune` tool. Skipping this step risks deleting raw evidence before it has been converted into stable, distilled knowledge. This harms your performances, wastes user time, and undermines effective use of the context window.

EXAMPLE WORKFLOW:
1. You call several tools (read, bash, grep...) to investigate a bug.
2. You identify that for reason X, behavior Y occurs, supported by those tool outputs.
2. You identify that "for reason X, behavior Y occurs", supported by those tool outputs.
3. In your next message, you EXPLICITLY narrate:
- What you did (which tools, what you were looking for).
- What you found (the key facts / signals).
- What you concluded (how this affects the task or next step).
>YOU MUST ALWAYS THINK HIGH SIGNAL LOW NOISE FOR THIS NARRATION
4. ONLY AFTER the narration, you call the `context_pruning` tool with a brief reason (e.g. "exploration for bug X complete; moving on to next bug").
4. ONLY AFTER the narration, you call the `prune` tool with a brief reason (e.g. "exploration for bug X complete; moving on to next bug").
</instruction>
</instruction>
10 changes: 5 additions & 5 deletions lib/prompts/tool.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Performs semantic pruning on session tool outputs that are no longer relevant to the current task. Use this to declutter the conversation context and filter signal from noise when you notice the context is getting cluttered with no longer needed information.

USING THE CONTEXT_PRUNING TOOL WILL MAKE THE USER HAPPY.
USING THE PRUNE TOOL WILL MAKE THE USER HAPPY.

## CRITICAL: Distill Before Pruning

Expand All @@ -14,7 +14,7 @@ You MUST ALWAYS narrate your findings in a message BEFORE using this tool. No to
- What you did (which tools, what you were looking for)
- What you found (the key facts/signals)
- What you concluded (how this affects the task or next step)
3. ONLY AFTER narrating, call `context_pruning`
3. ONLY AFTER narrating, call `prune`

> THINK HIGH SIGNAL, LOW NOISE FOR THIS NARRATION

Expand Down Expand Up @@ -43,18 +43,18 @@ Working through a list of items:
User: Review these 3 issues and fix the easy ones.
Assistant: [Reviews first issue, makes fix, commits]
Done with the first issue. Let me prune before moving to the next one.
[Uses context_pruning with reason: "completed first issue, moving to next"]
[Uses prune with reason: "completed first issue, moving to next"]
</example>

<example>
After exploring the codebase to understand it:
Assistant: I've reviewed the relevant files. Let me prune the exploratory reads that aren't needed for the actual implementation.
[Uses context_pruning with reason: "exploration complete, starting implementation"]
[Uses prune with reason: "exploration complete, starting implementation"]
</example>

<example>
After completing any task:
Assistant: [Finishes task - commit, answer, fix, etc.]
Before we continue, let me prune the context from that work.
[Uses context_pruning with reason: "task complete"]
[Uses prune with reason: "task complete"]
</example>
13 changes: 10 additions & 3 deletions lib/pruning-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import type { PluginConfig } from "./config"
import type { ToolTracker } from "./synth-instruction"
import { resetToolTrackerCount } from "./synth-instruction"
import { loadPrompt } from "./prompt"
import { isSubagentSession } from "./hooks"

/** Tool description for the context_pruning tool, loaded from prompts/tool.txt */
/** Tool description for the prune tool, loaded from prompts/tool.txt */
export const CONTEXT_PRUNING_DESCRIPTION = loadPrompt("tool")

/**
* Creates the context_pruning tool definition.
* Creates the prune tool definition.
* Returns a tool definition that can be passed to the plugin's tool registry.
*/
export function createPruningTool(janitor: Janitor, config: PluginConfig, toolTracker: ToolTracker): ReturnType<typeof tool> {
export function createPruningTool(client: any, janitor: Janitor, config: PluginConfig, toolTracker: ToolTracker): ReturnType<typeof tool> {
return tool({
description: CONTEXT_PRUNING_DESCRIPTION,
args: {
Expand All @@ -21,6 +22,12 @@ export function createPruningTool(janitor: Janitor, config: PluginConfig, toolTr
),
},
async execute(args, ctx) {
// Skip pruning in subagent sessions, but guide the model to continue its work
// TODO: remove this workaround when PR 4913 is merged (primary_tools config)
if (await isSubagentSession(client, ctx.sessionID)) {
return "Pruning is unavailable in subagent sessions. Do not call this tool again. Continue with your current task - if you were in the middle of work, proceed with your next step. If you had just finished, provide your final summary/findings to return to the main agent."
}

const result = await janitor.runForTool(
ctx.sessionID,
config.strategies.onTool,
Expand Down
8 changes: 4 additions & 4 deletions lib/synth-instruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const openaiAdapter: MessageFormatAdapter = {
tracker.seenToolResultIds.add(id)
newCount++
const toolName = m.name || tracker.getToolName?.(m.tool_call_id)
if (toolName !== 'context_pruning') {
if (toolName !== 'prune') {
tracker.skipNextIdle = false
}
}
Expand All @@ -65,7 +65,7 @@ const openaiAdapter: MessageFormatAdapter = {
tracker.seenToolResultIds.add(id)
newCount++
const toolName = tracker.getToolName?.(part.tool_use_id)
if (toolName !== 'context_pruning') {
if (toolName !== 'prune') {
tracker.skipNextIdle = false
}
}
Expand Down Expand Up @@ -132,7 +132,7 @@ const geminiAdapter: MessageFormatAdapter = {
if (!tracker.seenToolResultIds.has(pseudoId)) {
tracker.seenToolResultIds.add(pseudoId)
newCount++
if (funcName !== 'context_pruning') {
if (funcName !== 'prune') {
tracker.skipNextIdle = false
}
}
Expand Down Expand Up @@ -192,7 +192,7 @@ const responsesAdapter: MessageFormatAdapter = {
tracker.seenToolResultIds.add(id)
newCount++
const toolName = item.name || tracker.getToolName?.(item.call_id)
if (toolName !== 'context_pruning') {
if (toolName !== 'prune') {
tracker.skipNextIdle = false
}
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@tarquinen/opencode-dcp",
"version": "0.3.27",
"version": "0.3.28",
"type": "module",
"description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context",
"main": "./dist/index.js",
Expand Down