diff --git a/README.md b/README.md index 4a6ddb4..9d4f44f 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"] } ``` @@ -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,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. @@ -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"] } ``` diff --git a/index.ts b/index.ts index bed8c90..d7c06ba 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/config.ts b/lib/config.ts index 0b67071..1c70490 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/fetch-wrapper/gemini.ts b/lib/fetch-wrapper/gemini.ts index d57407d..d02bbd0 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 3f1265a..9aeb6d0 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 7631394..7741617 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 } } diff --git a/lib/hooks.ts b/lib/hooks.ts index fdbd6ae..6c2e807 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 c68fbcb..5a1fd5c 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 a2bc18e..50943e5 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 e71e7cb..579f510 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 fa953c8..5401988 100644 --- a/lib/pruning-tool.ts +++ b/lib/pruning-tool.ts @@ -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 { +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 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, diff --git a/lib/synth-instruction.ts b/lib/synth-instruction.ts index cb2a63b..aa2dd62 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 } } diff --git a/package-lock.json b/package-lock.json index 97c1196..b88cb2d 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 60d57a4..be2e5e8 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",