From 14fdcf6a2a096a3ff60f29a41d834e41b36db1b7 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Sun, 30 Nov 2025 23:38:02 -0500 Subject: [PATCH 1/7] docs: update README to reflect prune tool rename --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 4a6ddb45..d9d5897e 100644 --- a/README.md +++ b/README.md @@ -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. @@ -60,7 +60,7 @@ 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` | @@ -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"] } ``` From 58aeac6bc068f9ae08fdaece4b0df37f5cb45e9a Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Mon, 1 Dec 2025 14:54:24 -0500 Subject: [PATCH 2/7] chore: silence nudge and synthetic instruction injection logs --- lib/fetch-wrapper/gemini.ts | 4 ++-- lib/fetch-wrapper/openai-chat.ts | 4 ++-- lib/fetch-wrapper/openai-responses.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/fetch-wrapper/gemini.ts b/lib/fetch-wrapper/gemini.ts index d57407de..d02bbd00 100644 --- a/lib/fetch-wrapper/gemini.ts +++ b/lib/fetch-wrapper/gemini.ts @@ -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 } } @@ -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 } } diff --git a/lib/fetch-wrapper/openai-chat.ts b/lib/fetch-wrapper/openai-chat.ts index 3f1265a6..9aeb6d0a 100644 --- a/lib/fetch-wrapper/openai-chat.ts +++ b/lib/fetch-wrapper/openai-chat.ts @@ -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 } } @@ -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 } } diff --git a/lib/fetch-wrapper/openai-responses.ts b/lib/fetch-wrapper/openai-responses.ts index 7631394b..77416176 100644 --- a/lib/fetch-wrapper/openai-responses.ts +++ b/lib/fetch-wrapper/openai-responses.ts @@ -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 } } @@ -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 } } From 41e7ef049415696113d4dee2f50cd503b284c75b Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Mon, 1 Dec 2025 15:13:55 -0500 Subject: [PATCH 3/7] fix: disable pruning in subagent sessions and guide model to return summary --- index.ts | 2 +- lib/pruning-tool.ts | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/index.ts b/index.ts index bed8c906..d7c06ba6 100644 --- a/index.ts +++ b/index.ts @@ -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 diff --git a/lib/pruning-tool.ts b/lib/pruning-tool.ts index fa953c85..2fd268c0 100644 --- a/lib/pruning-tool.ts +++ b/lib/pruning-tool.ts @@ -4,6 +4,7 @@ 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 */ export const CONTEXT_PRUNING_DESCRIPTION = loadPrompt("tool") @@ -12,7 +13,7 @@ export const CONTEXT_PRUNING_DESCRIPTION = loadPrompt("tool") * Creates the context_pruning 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 { +export function createPruningTool(client: any, janitor: Janitor, config: PluginConfig, toolTracker: ToolTracker): ReturnType { return tool({ description: CONTEXT_PRUNING_DESCRIPTION, args: { @@ -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 provide a proper summary + // TODO: implement something better here when PR 4913 is merged + if (await isSubagentSession(client, ctx.sessionID)) { + return "Pruning is disabled in subagent sessions. IMPORTANT: You must now provide your final summary/findings to return to the main agent. Do not call this tool again - simply respond with your results." + } + const result = await janitor.runForTool( ctx.sessionID, config.strategies.onTool, From 06e0030231bc228db9872277d24fad807ba2b2ad Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Mon, 1 Dec 2025 15:20:59 -0500 Subject: [PATCH 4/7] refactor: rename context_pruning to prune throughout codebase --- README.md | 2 +- lib/config.ts | 2 +- lib/hooks.ts | 2 +- lib/prompts/nudge.txt | 2 +- lib/prompts/synthetic.txt | 15 ++++++++------- lib/prompts/tool.txt | 10 +++++----- lib/pruning-tool.ts | 4 ++-- lib/synth-instruction.ts | 8 ++++---- 8 files changed, 23 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index d9d5897e..1fd0f220 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.j | `nudge_freq` | `10` | How often to remind AI to prune (lower = more frequent) | | `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. diff --git a/lib/config.ts b/lib/config.ts index 0b670717..1c704909 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -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" diff --git a/lib/hooks.ts b/lib/hooks.ts index fdbd6ae2..6c2e8073 100644 --- a/lib/hooks.ts +++ b/lib/hooks.ts @@ -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 diff --git a/lib/prompts/nudge.txt b/lib/prompts/nudge.txt index c68fbcb1..5a1fd5c9 100644 --- a/lib/prompts/nudge.txt +++ b/lib/prompts/nudge.txt @@ -3,5 +3,5 @@ This nudge is injected by a plugin and is invisible to the user. Do not acknowle -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. diff --git a/lib/prompts/synthetic.txt b/lib/prompts/synthetic.txt index a2bc18e0..50943e58 100644 --- a/lib/prompts/synthetic.txt +++ b/lib/prompts/synthetic.txt @@ -9,29 +9,30 @@ THIS IS NON-NEGOTIABLE - YOU ARE EXPECTED TO RESPECT THIS INSTRUCTION THROUGHOUT -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. -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"). + diff --git a/lib/prompts/tool.txt b/lib/prompts/tool.txt index e71e7cbf..579f5109 100644 --- a/lib/prompts/tool.txt +++ b/lib/prompts/tool.txt @@ -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 @@ -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 @@ -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"] 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"] 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"] diff --git a/lib/pruning-tool.ts b/lib/pruning-tool.ts index 2fd268c0..fe76ddbf 100644 --- a/lib/pruning-tool.ts +++ b/lib/pruning-tool.ts @@ -6,11 +6,11 @@ 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(client: any, janitor: Janitor, config: PluginConfig, toolTracker: ToolTracker): ReturnType { diff --git a/lib/synth-instruction.ts b/lib/synth-instruction.ts index cb2a63b4..aa2dd628 100644 --- a/lib/synth-instruction.ts +++ b/lib/synth-instruction.ts @@ -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 } } @@ -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 } } @@ -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 } } @@ -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 } } From d456306bd8a18be04a3e848ebb77368cc6c8252c Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Mon, 1 Dec 2025 15:34:59 -0500 Subject: [PATCH 5/7] fix: improve subagent prune skip message to handle mid-workflow scenarios --- lib/pruning-tool.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/pruning-tool.ts b/lib/pruning-tool.ts index fe76ddbf..4a12c89c 100644 --- a/lib/pruning-tool.ts +++ b/lib/pruning-tool.ts @@ -22,10 +22,10 @@ export function createPruningTool(client: any, janitor: Janitor, config: PluginC ), }, async execute(args, ctx) { - // Skip pruning in subagent sessions, but guide the model to provide a proper summary - // TODO: implement something better here when PR 4913 is merged + // 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 disabled in subagent sessions. IMPORTANT: You must now provide your final summary/findings to return to the main agent. Do not call this tool again - simply respond with your results." + return "Pruning is unavailable in subagent sessions. 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( From bda794472cbb248ab0856fd9ef3fb0674ec8d7e4 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Mon, 1 Dec 2025 15:41:03 -0500 Subject: [PATCH 6/7] fix: add 'do not call this tool again' to subagent skip message --- lib/pruning-tool.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pruning-tool.ts b/lib/pruning-tool.ts index 4a12c89c..54019887 100644 --- a/lib/pruning-tool.ts +++ b/lib/pruning-tool.ts @@ -25,7 +25,7 @@ export function createPruningTool(client: any, janitor: Janitor, config: PluginC // 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. 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." + 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( From d4afd08df83252dbc351dc5a25336d4cb96730b7 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Mon, 1 Dec 2025 16:09:30 -0500 Subject: [PATCH 7/7] v0.3.28 - Bump version --- README.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1fd0f220..9d4f44ff 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Add to your OpenCode config: ```jsonc // opencode.jsonc { - "plugin": ["@tarquinen/opencode-dcp@0.3.27"] + "plugin": ["@tarquinen/opencode-dcp@0.3.28"] } ``` diff --git a/package-lock.json b/package-lock.json index 97c11969..b88cb2da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tarquinen/opencode-dcp", - "version": "0.3.27", + "version": "0.3.28", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tarquinen/opencode-dcp", - "version": "0.3.27", + "version": "0.3.28", "license": "MIT", "dependencies": { "@ai-sdk/openai-compatible": "^1.0.27", diff --git a/package.json b/package.json index 60d57a48..be2e5e85 100644 --- a/package.json +++ b/package.json @@ -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",