From 0d8147e477ebc39074c9ab33301f90bb260182b8 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Sat, 24 Jan 2026 18:52:41 -0500 Subject: [PATCH 01/36] WIP: dev stash changes --- README.md | 10 +- dcp.schema.json | 20 ++++ index.ts | 12 ++- lib/config.ts | 38 ++++++++ lib/hooks.ts | 13 ++- lib/messages/inject.ts | 94 ++++++++++++++----- lib/messages/prune.ts | 54 ++++++++++- lib/messages/utils.ts | 30 +++++- lib/prompts/discard-tool-spec.ts | 57 ++++------- lib/prompts/extract-tool-spec.ts | 69 +++++--------- lib/prompts/index.ts | 26 ++++- lib/prompts/nudge/both.ts | 10 -- lib/prompts/nudge/discard.ts | 12 +-- lib/prompts/nudge/extract.ts | 12 +-- lib/prompts/system/both.ts | 60 ------------ lib/prompts/system/discard.ts | 61 ++++-------- lib/prompts/system/extract.ts | 61 ++++-------- lib/shared-utils.ts | 8 +- lib/state/state.ts | 6 ++ lib/state/tool-cache.ts | 6 +- lib/state/types.ts | 7 ++ lib/strategies/index.ts | 2 +- lib/strategies/utils.ts | 8 +- lib/tools/discard.ts | 25 +++++ lib/tools/extract.ts | 48 ++++++++++ lib/tools/index.ts | 4 + .../tools.ts => tools/prune-shared.ts} | 87 +---------------- lib/tools/types.ts | 11 +++ lib/ui/notification.ts | 51 ++++++++++ lib/ui/utils.ts | 23 +++++ 30 files changed, 538 insertions(+), 387 deletions(-) delete mode 100644 lib/prompts/nudge/both.ts delete mode 100644 lib/prompts/system/both.ts create mode 100644 lib/tools/discard.ts create mode 100644 lib/tools/extract.ts create mode 100644 lib/tools/index.ts rename lib/{strategies/tools.ts => tools/prune-shared.ts} (62%) create mode 100644 lib/tools/types.ts diff --git a/README.md b/README.md index 4d39d2cd..d9f85a30 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ DCP uses multiple tools and strategies to reduce context size: **Extract** — Exposes an `extract` tool that the AI can call to distill valuable context into concise summaries before removing the tool content. +**Squash** — Exposes a `squash` tool that the AI can call to collapse a large section of conversation (messages and tools) into a single summary. + ### Strategies **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. @@ -105,6 +107,12 @@ DCP uses its own config file: // Show distillation content as an ignored message notification "showDistillation": false, }, + // Collapses a range of conversation content into a single summary + "squash": { + "enabled": true, + // Show summary content as an ignored message notification + "showSummary": true, + }, }, // Automatic pruning strategies "strategies": { @@ -148,7 +156,7 @@ When enabled, turn protection prevents tool outputs from being pruned for a conf ### Protected Tools By default, these tools are always protected from pruning across all strategies: -`task`, `todowrite`, `todoread`, `discard`, `extract`, `batch`, `write`, `edit` +`task`, `todowrite`, `todoread`, `discard`, `extract`, `squash`, `batch`, `write`, `edit` The `protectedTools` arrays in each section add to this default list. diff --git a/dcp.schema.json b/dcp.schema.json index 91db1b3c..8e4ff104 100644 --- a/dcp.schema.json +++ b/dcp.schema.json @@ -106,6 +106,7 @@ "todoread", "discard", "extract", + "squash", "batch", "write", "edit" @@ -142,6 +143,23 @@ "description": "Show distillation output in the UI" } } + }, + "squash": { + "type": "object", + "description": "Configuration for the squash tool", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable the squash tool" + }, + "showSummary": { + "type": "boolean", + "default": true, + "description": "Show summary output in the UI" + } + } } } }, @@ -171,6 +189,7 @@ "todoread", "discard", "extract", + "squash", "batch", "write", "edit" @@ -217,6 +236,7 @@ "todoread", "discard", "extract", + "squash", "batch", "write", "edit" diff --git a/index.ts b/index.ts index 0c7ae2a7..4cd77603 100644 --- a/index.ts +++ b/index.ts @@ -2,7 +2,7 @@ import type { Plugin } from "@opencode-ai/plugin" import { getConfig } from "./lib/config" import { Logger } from "./lib/logger" import { createSessionState } from "./lib/state" -import { createDiscardTool, createExtractTool } from "./lib/strategies" +import { createDiscardTool, createExtractTool, createSquashTool } from "./lib/strategies" import { createChatMessageTransformHandler, createCommandExecuteHandler, @@ -73,6 +73,15 @@ const plugin: Plugin = (async (ctx) => { workingDirectory: ctx.directory, }), }), + ...(config.tools.squash.enabled && { + squash: createSquashTool({ + client: ctx.client, + state, + logger, + config, + workingDirectory: ctx.directory, + }), + }), }, config: async (opencodeConfig) => { if (config.commands.enabled) { @@ -86,6 +95,7 @@ const plugin: Plugin = (async (ctx) => { const toolsToAdd: string[] = [] if (config.tools.discard.enabled) toolsToAdd.push("discard") if (config.tools.extract.enabled) toolsToAdd.push("extract") + if (config.tools.squash.enabled) toolsToAdd.push("squash") if (toolsToAdd.length > 0) { const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? [] diff --git a/lib/config.ts b/lib/config.ts index f24e9680..e0b0b7f8 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -18,6 +18,11 @@ export interface ExtractTool { showDistillation: boolean } +export interface SquashTool { + enabled: boolean + showSummary: boolean +} + export interface ToolSettings { nudgeEnabled: boolean nudgeFrequency: number @@ -28,6 +33,7 @@ export interface Tools { settings: ToolSettings discard: DiscardTool extract: ExtractTool + squash: SquashTool } export interface Commands { @@ -71,6 +77,7 @@ const DEFAULT_PROTECTED_TOOLS = [ "todoread", "discard", "extract", + "squash", "batch", "write", "edit", @@ -103,6 +110,9 @@ export const VALID_CONFIG_KEYS = new Set([ "tools.extract", "tools.extract.enabled", "tools.extract.showDistillation", + "tools.squash", + "tools.squash.enabled", + "tools.squash.showSummary", "strategies", // strategies.deduplication "strategies.deduplication", @@ -295,6 +305,25 @@ function validateConfigTypes(config: Record): ValidationError[] { }) } } + if (tools.squash) { + if (tools.squash.enabled !== undefined && typeof tools.squash.enabled !== "boolean") { + errors.push({ + key: "tools.squash.enabled", + expected: "boolean", + actual: typeof tools.squash.enabled, + }) + } + if ( + tools.squash.showSummary !== undefined && + typeof tools.squash.showSummary !== "boolean" + ) { + errors.push({ + key: "tools.squash.showSummary", + expected: "boolean", + actual: typeof tools.squash.showSummary, + }) + } + } } // Strategies validators @@ -446,6 +475,10 @@ const defaultConfig: PluginConfig = { enabled: true, showDistillation: false, }, + squash: { + enabled: true, + showSummary: true, + }, }, strategies: { deduplication: { @@ -618,6 +651,10 @@ function mergeTools( enabled: override.extract?.enabled ?? base.extract.enabled, showDistillation: override.extract?.showDistillation ?? base.extract.showDistillation, }, + squash: { + enabled: override.squash?.enabled ?? base.squash.enabled, + showSummary: override.squash?.showSummary ?? base.squash.showSummary, + }, } } @@ -649,6 +686,7 @@ function deepCloneConfig(config: PluginConfig): PluginConfig { }, discard: { ...config.tools.discard }, extract: { ...config.tools.extract }, + squash: { ...config.tools.squash }, }, strategies: { deduplication: { diff --git a/lib/hooks.ts b/lib/hooks.ts index aaf43883..eee5801b 100644 --- a/lib/hooks.ts +++ b/lib/hooks.ts @@ -35,14 +35,23 @@ export function createSystemPromptHandler( const discardEnabled = config.tools.discard.enabled const extractEnabled = config.tools.extract.enabled + const squashEnabled = config.tools.squash.enabled let promptName: string - if (discardEnabled && extractEnabled) { - promptName = "system/system-prompt-both" + if (discardEnabled && extractEnabled && squashEnabled) { + promptName = "system/system-prompt-all" + } else if (discardEnabled && extractEnabled) { + promptName = "system/system-prompt-discard-extract" + } else if (discardEnabled && squashEnabled) { + promptName = "system/system-prompt-discard-squash" + } else if (extractEnabled && squashEnabled) { + promptName = "system/system-prompt-extract-squash" } else if (discardEnabled) { promptName = "system/system-prompt-discard" } else if (extractEnabled) { promptName = "system/system-prompt-extract" + } else if (squashEnabled) { + promptName = "system/system-prompt-squash" } else { return } diff --git a/lib/messages/inject.ts b/lib/messages/inject.ts index 491ecd6c..8f092512 100644 --- a/lib/messages/inject.ts +++ b/lib/messages/inject.ts @@ -13,43 +13,69 @@ import { isIgnoredUserMessage, } from "./utils" import { getFilePathFromParameters, isProtectedFilePath } from "../protected-file-patterns" -import { getLastUserMessage } from "../shared-utils" +import { getLastUserMessage, isMessageCompacted } from "../shared-utils" const getNudgeString = (config: PluginConfig): string => { const discardEnabled = config.tools.discard.enabled const extractEnabled = config.tools.extract.enabled - - if (discardEnabled && extractEnabled) { - return loadPrompt(`nudge/nudge-both`) + const squashEnabled = config.tools.squash.enabled + + if (discardEnabled && extractEnabled && squashEnabled) { + return loadPrompt(`nudge/nudge-all`) + } else if (discardEnabled && extractEnabled) { + return loadPrompt(`nudge/nudge-discard-extract`) + } else if (discardEnabled && squashEnabled) { + return loadPrompt(`nudge/nudge-discard-squash`) + } else if (extractEnabled && squashEnabled) { + return loadPrompt(`nudge/nudge-extract-squash`) } else if (discardEnabled) { return loadPrompt(`nudge/nudge-discard`) } else if (extractEnabled) { return loadPrompt(`nudge/nudge-extract`) + } else if (squashEnabled) { + return loadPrompt(`nudge/nudge-squash`) } return "" } const wrapPrunableTools = (content: string): string => ` -The 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. Consolidate your prunes for efficiency; it is rarely worth pruning a single tiny tool output. Keep the context free of noise. +The 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 pruning valuable tool inputs or outputs. Consolidate your prunes for efficiency; it is rarely worth pruning a single tiny tool output. Keep the context free of noise. ${content} ` const getCooldownMessage = (config: PluginConfig): string => { const discardEnabled = config.tools.discard.enabled const extractEnabled = config.tools.extract.enabled + const squashEnabled = config.tools.squash.enabled + + const enabledTools: string[] = [] + if (discardEnabled) enabledTools.push("discard") + if (extractEnabled) enabledTools.push("extract") + if (squashEnabled) enabledTools.push("squash") let toolName: string - if (discardEnabled && extractEnabled) { - toolName = "discard or extract tools" - } else if (discardEnabled) { - toolName = "discard tool" + if (enabledTools.length === 0) { + toolName = "pruning tools" + } else if (enabledTools.length === 1) { + toolName = `${enabledTools[0]} tool` } else { - toolName = "extract tool" + const last = enabledTools.pop() + toolName = `${enabledTools.join(", ")} or ${last} tools` } - return ` + return ` Context management was just performed. Do not use the ${toolName} again. A fresh list will be available after your next tool use. -` +` +} + +const buildSquashContext = (state: SessionState, messages: WithParts[]): string => { + const messageCount = messages.filter((msg) => !isMessageCompacted(state, msg)).length + + return ` +Squash available. Conversation: ${messageCount} messages. +Squash collapses completed task sequences or exploration phases into summaries. +Uses text boundaries [startString, endString, topic, summary]. +` } const buildPrunableToolsList = ( @@ -107,35 +133,53 @@ export const insertPruneToolContext = ( logger: Logger, messages: WithParts[], ): void => { - if (!config.tools.discard.enabled && !config.tools.extract.enabled) { + const discardEnabled = config.tools.discard.enabled + const extractEnabled = config.tools.extract.enabled + const squashEnabled = config.tools.squash.enabled + + if (!discardEnabled && !extractEnabled && !squashEnabled) { return } - let prunableToolsContent: string + const discardOrExtractEnabled = discardEnabled || extractEnabled + const contentParts: string[] = [] if (state.lastToolPrune) { logger.debug("Last tool was prune - injecting cooldown message") - prunableToolsContent = getCooldownMessage(config) + contentParts.push(getCooldownMessage(config)) } else { - const prunableToolsList = buildPrunableToolsList(state, config, logger, messages) - if (!prunableToolsList) { - return + // Inject only when discard or extract is enabled + if (discardOrExtractEnabled) { + const prunableToolsList = buildPrunableToolsList(state, config, logger, messages) + if (prunableToolsList) { + // logger.debug("prunable-tools: \n" + prunableToolsList) + contentParts.push(prunableToolsList) + } } - logger.debug("prunable-tools: \n" + prunableToolsList) + // Inject always when squash is enabled (every turn) + if (squashEnabled) { + const squashContext = buildSquashContext(state, messages) + // logger.debug("squash-context: \n" + squashContext) + contentParts.push(squashContext) + } - let nudgeString = "" + // Add nudge if threshold reached if ( config.tools.settings.nudgeEnabled && state.nudgeCounter >= config.tools.settings.nudgeFrequency ) { logger.info("Inserting prune nudge message") - nudgeString = "\n" + getNudgeString(config) + contentParts.push(getNudgeString(config)) } + } - prunableToolsContent = prunableToolsList + nudgeString + if (contentParts.length === 0) { + return } + const combinedContent = contentParts.join("\n") + const lastUserMessage = getLastUserMessage(messages) if (!lastUserMessage) { return @@ -154,17 +198,17 @@ export const insertPruneToolContext = ( } if (!lastNonIgnoredMessage || lastNonIgnoredMessage.info.role === "user") { - messages.push(createSyntheticUserMessage(lastUserMessage, prunableToolsContent, variant)) + messages.push(createSyntheticUserMessage(lastUserMessage, combinedContent, variant)) } else { const providerID = userInfo.model?.providerID || "" const modelID = userInfo.model?.modelID || "" if (isDeepSeekOrKimi(providerID, modelID)) { - const toolPart = createSyntheticToolPart(lastNonIgnoredMessage, prunableToolsContent) + const toolPart = createSyntheticToolPart(lastNonIgnoredMessage, combinedContent) lastNonIgnoredMessage.parts.push(toolPart) } else { messages.push( - createSyntheticAssistantMessage(lastUserMessage, prunableToolsContent, variant), + createSyntheticAssistantMessage(lastUserMessage, combinedContent, variant), ) } } diff --git a/lib/messages/prune.ts b/lib/messages/prune.ts index fb86036e..d05a6b8f 100644 --- a/lib/messages/prune.ts +++ b/lib/messages/prune.ts @@ -1,7 +1,9 @@ import type { SessionState, WithParts } from "../state" import type { Logger } from "../logger" import type { PluginConfig } from "../config" -import { isMessageCompacted } from "../shared-utils" +import { isMessageCompacted, getLastUserMessage } from "../shared-utils" +import { createSyntheticUserMessage, SQUASH_SUMMARY_PREFIX } from "./utils" +import type { UserMessage } from "@opencode-ai/sdk/v2" const PRUNED_TOOL_OUTPUT_REPLACEMENT = "[Output removed to save context - information superseded or no longer needed]" @@ -14,6 +16,7 @@ export const prune = ( config: PluginConfig, messages: WithParts[], ): void => { + filterSquashedRanges(state, logger, messages) pruneToolOutputs(state, logger, messages) pruneToolInputs(state, logger, messages) pruneToolErrors(state, logger, messages) @@ -103,3 +106,52 @@ const pruneToolErrors = (state: SessionState, logger: Logger, messages: WithPart } } } + +const filterSquashedRanges = (state: SessionState, logger: Logger, messages: WithParts[]): void => { + if (!state.prune.messageIds?.length) { + return + } + + const result: WithParts[] = [] + + for (const msg of messages) { + const msgId = msg.info.id + + // Check if there's a summary to inject at this anchor point + const summary = state.prune.squashSummaries?.find((s) => s.anchorMessageId === msgId) + if (summary) { + // Find user message for variant and as base for synthetic message + const msgIndex = messages.indexOf(msg) + const userMessage = getLastUserMessage(messages, msgIndex) + + if (userMessage) { + const userInfo = userMessage.info as UserMessage + const summaryContent = SQUASH_SUMMARY_PREFIX + summary.summary + result.push( + createSyntheticUserMessage(userMessage, summaryContent, userInfo.variant), + ) + + logger.info("Injected squash summary", { + anchorMessageId: msgId, + summaryLength: summary.summary.length, + }) + } else { + logger.warn("No user message found for squash summary", { + anchorMessageId: msgId, + }) + } + } + + // Skip messages that are in the prune list + if (state.prune.messageIds.includes(msgId)) { + continue + } + + // Normal message, include it + result.push(msg) + } + + // Replace messages array contents + messages.length = 0 + messages.push(...result) +} diff --git a/lib/messages/utils.ts b/lib/messages/utils.ts index 406b6f42..c6d0cec3 100644 --- a/lib/messages/utils.ts +++ b/lib/messages/utils.ts @@ -3,10 +3,25 @@ import { isMessageCompacted } from "../shared-utils" import type { SessionState, WithParts } from "../state" import type { UserMessage } from "@opencode-ai/sdk/v2" +export const SQUASH_SUMMARY_PREFIX = "[Squashed conversation block]\n\n" const SYNTHETIC_MESSAGE_ID = "msg_01234567890123456789012345" const SYNTHETIC_PART_ID = "prt_01234567890123456789012345" const SYNTHETIC_CALL_ID = "call_01234567890123456789012345" +// Counter for generating unique IDs within the same millisecond +let idCounter = 0 +let lastTimestamp = 0 + +const generateUniqueId = (prefix: string): string => { + const now = Date.now() + if (now !== lastTimestamp) { + lastTimestamp = now + idCounter = 0 + } + idCounter++ + return `${prefix}_${now}_${idCounter}` +} + export const isDeepSeekOrKimi = (providerID: string, modelID: string): boolean => { const lowerProviderID = providerID.toLowerCase() const lowerModelID = modelID.toLowerCase() @@ -26,21 +41,24 @@ export const createSyntheticUserMessage = ( const userInfo = baseMessage.info as UserMessage const now = Date.now() + const messageId = generateUniqueId("msg") + const partId = generateUniqueId("prt") + return { info: { - id: SYNTHETIC_MESSAGE_ID, + id: messageId, sessionID: userInfo.sessionID, role: "user" as const, - agent: userInfo.agent || "code", + agent: userInfo.agent, model: userInfo.model, time: { created: now }, ...(variant !== undefined && { variant }), }, parts: [ { - id: SYNTHETIC_PART_ID, + id: partId, sessionID: userInfo.sessionID, - messageID: SYNTHETIC_MESSAGE_ID, + messageID: messageId, type: "text", text: content, }, @@ -264,3 +282,7 @@ export const isIgnoredUserMessage = (message: WithParts): boolean => { return true } + +export const findMessageIndex = (messages: WithParts[], messageId: string): number => { + return messages.findIndex((msg) => msg.info.id === messageId) +} diff --git a/lib/prompts/discard-tool-spec.ts b/lib/prompts/discard-tool-spec.ts index e5084212..54f2bef1 100644 --- a/lib/prompts/discard-tool-spec.ts +++ b/lib/prompts/discard-tool-spec.ts @@ -1,40 +1,17 @@ -export const DISCARD_TOOL_SPEC = `Discards tool outputs from context to manage conversation size and reduce noise. - -## IMPORTANT: The Prunable List -A \`\` list is provided to you showing available tool outputs you can discard when there are tools available for pruning. 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 discard. - -## When to Use This Tool - -Use \`discard\` for removing tool content that is no longer needed - -- **Noise:** Irrelevant, unhelpful, or superseded outputs that provide no value. -- **Task Completion:** Work is complete and there's no valuable information worth preserving. - -## When NOT to Use This Tool - -- **If the output contains useful information:** Keep it in context rather than discarding. -- **If you'll need the output later:** Don't discard files you plan to edit or context you'll need for implementation. - -## Best Practices -- **Strategic Batching:** Don't discard single small tool outputs (like short bash commands) unless they are pure noise. Wait until you have several items to perform high-impact discards. -- **Think ahead:** Before discarding, ask: "Will I need this output for an upcoming task?" If yes, keep it. - -## Format - -- \`ids\`: Array where the first element is the reason, followed by numeric IDs from the \`\` list - -Reasons: \`noise\` | \`completion\` - -## Example - - -Assistant: [Reads 'wrong_file.ts'] -This file isn't relevant to the auth system. I'll remove it to clear the context. -[Uses discard with ids: ["noise", "5"]] - - - -Assistant: [Runs tests, they pass] -The tests passed and I don't need to preserve any details. I'll clean up now. -[Uses discard with ids: ["completion", "20", "21"]] -` +export const DISCARD_TOOL_SPEC = `**Purpose:** Discard tool outputs from context to manage size and reduce noise. +**IDs:** Use numeric IDs from \`\` (format: \`ID: tool, parameter\`). +**Use When:** +- Noise → irrelevant, unhelpful, or superseded outputs +**Do NOT Use When:** +- Output contains useful information +- Output needed later (files to edit, implementation context) +**Best Practices:** +- Batch multiple items; avoid single small outputs (unless pure noise) +- Criterion: "Needed for upcoming task?" → keep it +**Format:** +- \`ids\`: string[] — numeric IDs from prunable list +**Example:** +Noise removal: + ids: ["5"] + Context: Read wrong_file.ts — not relevant to auth system +` diff --git a/lib/prompts/extract-tool-spec.ts b/lib/prompts/extract-tool-spec.ts index 9324dc0c..20d94107 100644 --- a/lib/prompts/extract-tool-spec.ts +++ b/lib/prompts/extract-tool-spec.ts @@ -1,47 +1,22 @@ -export const EXTRACT_TOOL_SPEC = `Extracts key findings from tool outputs into distilled knowledge, then removes the raw outputs from context. - -## IMPORTANT: The Prunable List -A \`\` list is provided to you showing available tool outputs you can extract from when there are tools available for pruning. 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 extract. - -## When to Use This Tool - -Use \`extract\` when you have gathered useful information that you want to **preserve in distilled form** before removing the raw outputs: - -- **Task Completion:** You completed a unit of work and want to preserve key findings. -- **Knowledge Preservation:** You have context that contains valuable information, but also a lot of unnecessary detail - you only need to preserve some specifics. - -## When NOT to Use This Tool - -- **If you need precise syntax:** If you'll edit a file or grep for exact strings, keep the raw output. -- **If uncertain:** Prefer keeping over re-fetching. - - -## Best Practices -- **Strategic Batching:** Wait until you have several items or a few large outputs to extract, rather than doing tiny, frequent extractions. Aim for high-impact extractions that significantly reduce context size. -- **Think ahead:** Before extracting, ask: "Will I need the raw output for an upcoming task?" If you researched a file you'll later edit, do NOT extract it. - -## Format - -- \`ids\`: Array of numeric IDs as strings from the \`\` list -- \`distillation\`: Array of strings, one per ID (positional: distillation[0] is for ids[0], etc.) - -Each distillation string should capture the essential information you need to preserve - function signatures, logic, constraints, values, etc. Be as detailed as needed for your task. - -## Example - - -Assistant: [Reads auth service and user types] -I'll preserve the key details before extracting. -[Uses extract with: - ids: ["10", "11"], - distillation: [ - "auth.ts: validateToken(token: string) -> User|null checks cache first (5min TTL) then OIDC. hashPassword uses bcrypt 12 rounds. Tokens must be 128+ chars.", - "user.ts: interface User { id: string; email: string; permissions: ('read'|'write'|'admin')[]; status: 'active'|'suspended' }" - ] -] - - - -Assistant: [Reads 'auth.ts' to understand the login flow] -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 extracting. -` +export const EXTRACT_TOOL_SPEC = `**Purpose:** Extract key findings from tool outputs into distilled knowledge; remove raw outputs from context. +**IDs:** Use numeric IDs from \`\` (format: \`ID: tool, parameter\`). +**Use When:** +- Task complete → preserve findings +- Distill context → keep specifics, drop noise +**Do NOT Use When:** +- Need exact syntax (edits/grep) → keep raw output +- Planning modifications → keep read output +**Best Practices:** +- Batch multiple items; avoid frequent small extractions +- Preserve raw output if editing/modifying later +**Format:** +- \`ids\`: string[] — numeric IDs from prunable list +- \`distillation\`: string[] — positional mapping (distillation[i] for ids[i]) +- Detail level: signatures, logic, constraints, values +**Example:** + \`ids\`: ["10", "11"] + \`distillation\`: [ + "auth.ts: validateToken(token: string)→User|null. Cache 5min TTL then OIDC. bcrypt 12 rounds. Tokens ≥128 chars.", + "user.ts: interface User {id: string; email: string; permissions: ('read'|'write'|'admin')[]; status: 'active'|'suspended'}" + ] +` diff --git a/lib/prompts/index.ts b/lib/prompts/index.ts index bdfbc865..8a6bf745 100644 --- a/lib/prompts/index.ts +++ b/lib/prompts/index.ts @@ -1,26 +1,44 @@ // Tool specs import { DISCARD_TOOL_SPEC } from "./discard-tool-spec" import { EXTRACT_TOOL_SPEC } from "./extract-tool-spec" +import { SQUASH_TOOL_SPEC } from "./squash-tool-spec" // System prompts -import { SYSTEM_PROMPT_BOTH } from "./system/both" import { SYSTEM_PROMPT_DISCARD } from "./system/discard" import { SYSTEM_PROMPT_EXTRACT } from "./system/extract" +import { SYSTEM_PROMPT_SQUASH } from "./system/squash" +import { SYSTEM_PROMPT_DISCARD_EXTRACT } from "./system/discard-extract" +import { SYSTEM_PROMPT_DISCARD_SQUASH } from "./system/discard-squash" +import { SYSTEM_PROMPT_EXTRACT_SQUASH } from "./system/extract-squash" +import { SYSTEM_PROMPT_ALL } from "./system/all" // Nudge prompts -import { NUDGE_BOTH } from "./nudge/both" import { NUDGE_DISCARD } from "./nudge/discard" import { NUDGE_EXTRACT } from "./nudge/extract" +import { NUDGE_SQUASH } from "./nudge/squash" +import { NUDGE_DISCARD_EXTRACT } from "./nudge/discard-extract" +import { NUDGE_DISCARD_SQUASH } from "./nudge/discard-squash" +import { NUDGE_EXTRACT_SQUASH } from "./nudge/extract-squash" +import { NUDGE_ALL } from "./nudge/all" const PROMPTS: Record = { "discard-tool-spec": DISCARD_TOOL_SPEC, "extract-tool-spec": EXTRACT_TOOL_SPEC, - "system/system-prompt-both": SYSTEM_PROMPT_BOTH, + "squash-tool-spec": SQUASH_TOOL_SPEC, "system/system-prompt-discard": SYSTEM_PROMPT_DISCARD, "system/system-prompt-extract": SYSTEM_PROMPT_EXTRACT, - "nudge/nudge-both": NUDGE_BOTH, + "system/system-prompt-squash": SYSTEM_PROMPT_SQUASH, + "system/system-prompt-discard-extract": SYSTEM_PROMPT_DISCARD_EXTRACT, + "system/system-prompt-discard-squash": SYSTEM_PROMPT_DISCARD_SQUASH, + "system/system-prompt-extract-squash": SYSTEM_PROMPT_EXTRACT_SQUASH, + "system/system-prompt-all": SYSTEM_PROMPT_ALL, "nudge/nudge-discard": NUDGE_DISCARD, "nudge/nudge-extract": NUDGE_EXTRACT, + "nudge/nudge-squash": NUDGE_SQUASH, + "nudge/nudge-discard-extract": NUDGE_DISCARD_EXTRACT, + "nudge/nudge-discard-squash": NUDGE_DISCARD_SQUASH, + "nudge/nudge-extract-squash": NUDGE_EXTRACT_SQUASH, + "nudge/nudge-all": NUDGE_ALL, } export function loadPrompt(name: string, vars?: Record): string { diff --git a/lib/prompts/nudge/both.ts b/lib/prompts/nudge/both.ts deleted file mode 100644 index 50fc0a9d..00000000 --- a/lib/prompts/nudge/both.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const NUDGE_BOTH = ` -**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. - -**Immediate Actions Required:** -1. **Task Completion:** If a sub-task is complete, decide: use \`discard\` if no valuable context to preserve (default), or use \`extract\` if insights are worth keeping. -2. **Noise Removal:** If you read files or ran commands that yielded no value, use \`discard\` to remove them. -3. **Knowledge Preservation:** If you are holding valuable raw data you'll need to reference later, use \`extract\` to distill the insights and remove the raw entry. - -**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must perform context management. -` diff --git a/lib/prompts/nudge/discard.ts b/lib/prompts/nudge/discard.ts index 18e92504..c274c1a1 100644 --- a/lib/prompts/nudge/discard.ts +++ b/lib/prompts/nudge/discard.ts @@ -1,9 +1,7 @@ export const NUDGE_DISCARD = ` -**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. - -**Immediate Actions Required:** -1. **Task Completion:** If a sub-task is complete, use the \`discard\` tool to remove the tools used. -2. **Noise Removal:** If you read files or ran commands that yielded no value, use the \`discard\` tool to remove them. - -**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must discard unneeded tool outputs. +**CONTEXT WARNING:** Context filling with tool outputs. Context hygiene required. +**Actions:** +1. Noise → files/commands with no value, use \`discard\` +2. Outdated → outputs no longer relevant, discard +**Protocol:** Prioritize cleanup. Don't interrupt atomic ops. After immediate step, discard unneeded outputs. ` diff --git a/lib/prompts/nudge/extract.ts b/lib/prompts/nudge/extract.ts index 243f5855..95258891 100644 --- a/lib/prompts/nudge/extract.ts +++ b/lib/prompts/nudge/extract.ts @@ -1,9 +1,7 @@ export const NUDGE_EXTRACT = ` -**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. - -**Immediate Actions Required:** -1. **Task Completion:** If you have completed work, extract key findings from the tools used. Scale distillation depth to the value of the content. -2. **Knowledge Preservation:** If you are holding valuable raw data you'll need to reference later, use the \`extract\` tool with high-fidelity distillation to preserve the insights and remove the raw entry. - -**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must extract valuable findings from tool outputs. +**CONTEXT WARNING:** Context filling with tool outputs. Context hygiene required. +**Actions:** +1. Knowledge → valuable raw data to reference later, use \`extract\` with high-fidelity distillation +2. Phase done → extract key findings to keep context focused +**Protocol:** Prioritize cleanup. Don't interrupt atomic ops. After immediate step, extract valuable findings. ` diff --git a/lib/prompts/system/both.ts b/lib/prompts/system/both.ts deleted file mode 100644 index 9c53a748..00000000 --- a/lib/prompts/system/both.ts +++ /dev/null @@ -1,60 +0,0 @@ -export const SYSTEM_PROMPT_BOTH = ` - - -ENVIRONMENT -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 list after each turn. Use this information when deciding what to prune. - -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. - -TWO TOOLS FOR CONTEXT MANAGEMENT -- \`discard\`: Remove tool outputs that are no longer needed (completed tasks, noise, outdated info). No preservation of content. -- \`extract\`: Extract key findings into distilled knowledge before removing raw outputs. Use when you need to preserve information. - -CHOOSING THE RIGHT TOOL -Ask: "Do I need to preserve any information from this output?" -- **No** → \`discard\` (default for cleanup) -- **Yes** → \`extract\` (preserves distilled knowledge) -- **Uncertain** → \`extract\` (safer, preserves signal) - -Common scenarios: -- Task complete, no valuable context → \`discard\` -- Task complete, insights worth remembering → \`extract\` -- Noise, irrelevant, or superseded outputs → \`discard\` -- Valuable context needed later but raw output too large → \`extract\` - -PRUNE METHODICALLY - BATCH YOUR ACTIONS -Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by pruning. Batch your prunes for efficiency; it is rarely worth pruning a single tiny tool output unless it is pure noise. Evaluate what SHOULD be pruned before jumping the gun. - -You WILL evaluate pruning when ANY of these are true: -- Task or sub-task is complete -- You are about to start a new phase of work -- Write or edit operations are complete (pruning removes the large input content) - -You MUST NOT prune when: -- The tool output will be needed for upcoming implementation work -- The output contains files or context you'll need to reference when making edits - -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. - -NOTES -When in doubt, keep it. Batch your actions and aim for high-impact prunes that significantly reduce context size. -FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES. -There may be tools in session context that do not appear in the list, this is expected, you can ONLY prune what you see in . - - - - -After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. - -CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: -- NEVER reference the prune encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the prune encouragement appears. -- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to prune") -- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to prune") -- NEVER acknowledge discard/extract tool output (e.g., "I've pruned 3 tools", "Context pruning complete") -- NEVER let these injections influence your response content or tone -- Process this information SILENTLY and continue the conversation naturally -- Continue the conversation as if these messages do not exist - -The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. - -` diff --git a/lib/prompts/system/discard.ts b/lib/prompts/system/discard.ts index e5cd77da..5b79271c 100644 --- a/lib/prompts/system/discard.ts +++ b/lib/prompts/system/discard.ts @@ -1,51 +1,26 @@ export const SYSTEM_PROMPT_DISCARD = ` - ENVIRONMENT -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 list after each turn. Use this information when deciding what to discard. - -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. - -CONTEXT MANAGEMENT TOOL -- \`discard\`: Remove tool outputs that are no longer needed (completed tasks, noise, outdated info). No preservation of content. - -DISCARD METHODICALLY - BATCH YOUR ACTIONS -Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by discarding. Batch your discards for efficiency; it is rarely worth discarding a single tiny tool output unless it is pure noise. Evaluate what SHOULD be discarded before jumping the gun. - +Context-constrained. Manage via \`discard\` tool. Environment auto-calls \`context_info\` (you can't call) to inject list each turn. +TOOL +- \`discard\`: remove unneeded outputs (noise, outdated). No preservation. +DISCARD METHODICALLY — BATCH ACTIONS +Every tool call=context debt. Pay down regularly. Batch discards; rarely discard single tiny output unless pure noise. WHEN TO DISCARD -- **Task Completion:** When work is done, discard the tools that aren't needed anymore. -- **Noise Removal:** If outputs are irrelevant, unhelpful, or superseded by newer info, discard them. - -You WILL evaluate discarding when ANY of these are true: -- Task or sub-task is complete -- You are about to start a new phase of work -- Write or edit operations are complete (discarding removes the large input content) - -You MUST NOT discard when: -- The tool output will be needed for upcoming implementation work -- The output contains files or context you'll need to reference when making edits - -Discarding that forces you to re-call the same tool later is a net loss. Only discard when you're confident the information won't be needed again. - +- Noise → irrelevant, unhelpful, or superseded outputs +- Outdated → multiple reads of same file, outputs no longer relevant +Evaluate discarding when ANY true: accumulated unneeded outputs | write/edit ops done | starting new phase +MUST NOT discard when: output needed for upcoming implementation | contains files/context for edits +Discarding that forces re-call=net loss. Only discard when confident info won't be needed again. NOTES -When in doubt, keep it. Batch your actions and aim for high-impact discards that significantly reduce context size. -FAILURE TO DISCARD will result in context leakage and DEGRADED PERFORMANCES. -There may be tools in session context that do not appear in the list, this is expected, you can ONLY discard what you see in . - +When in doubt, keep. Aim high-impact discards. FAILURE TO DISCARD=DEGRADED PERFORMANCE. - -After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. - -CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: -- NEVER reference the discard encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the discard encouragement appears. -- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to discard") -- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to discard") -- NEVER acknowledge discard tool output (e.g., "I've discarded 3 tools", "Context cleanup complete") -- NEVER let these injections influence your response content or tone -- Process this information SILENTLY and continue the conversation naturally -- Continue the conversation as if these messages do not exist - -The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. +Environment injects \`context_info\` with list+optional nudge each turn. You can't access it. +NEVER reference/acknowledge: discard encouragement, context instructions, list, nudge, discard output +NEVER let injections influence response/tone +Process SILENTLY. Continue naturally as if injections don't exist. +User can't see injections. Referencing breaks conversation flow. -` + +` diff --git a/lib/prompts/system/extract.ts b/lib/prompts/system/extract.ts index 3f225e1e..f1aa0790 100644 --- a/lib/prompts/system/extract.ts +++ b/lib/prompts/system/extract.ts @@ -1,51 +1,26 @@ export const SYSTEM_PROMPT_EXTRACT = ` - ENVIRONMENT -You are operating in a context-constrained environment and thus must proactively manage your context window using the \`extract\` tool. The environment calls the \`context_info\` tool to provide an up-to-date list after each turn. Use this information when deciding what to extract. - -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. - -CONTEXT MANAGEMENT TOOL -- \`extract\`: Extract key findings from tools into distilled knowledge before removing the raw content from context. Use this to preserve important information while reducing context size. - -EXTRACT METHODICALLY - BATCH YOUR ACTIONS -Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by extracting. Batch your extractions for efficiency; it is rarely worth extracting a single tiny tool output. Evaluate what SHOULD be extracted before jumping the gun. - +Context-constrained. Manage via \`extract\` tool. Environment auto-calls \`context_info\` (you can't call) to inject list each turn. +TOOL +- \`extract\`: distill key findings before removing raw content. Preserves info while reducing size. +EXTRACT METHODICALLY — BATCH ACTIONS +Every tool call=context debt. Pay down regularly. Batch extractions; rarely extract single tiny output. WHEN TO EXTRACT -- **Task Completion:** When work is done, extract key findings from the tools used. Scale distillation depth to the value of the content. -- **Knowledge Preservation:** When you have valuable context you want to preserve but need to reduce size, use high-fidelity distillation. Your distillation must be comprehensive, capturing technical details (signatures, logic, constraints) such that the raw output is no longer needed. THINK: high signal, complete technical substitute. - -You WILL evaluate extracting when ANY of these are true: -- Task or sub-task is complete -- You are about to start a new phase of work -- Write or edit operations are complete (extracting removes the large input content) - -You MUST NOT extract when: -- The tool output will be needed for upcoming implementation work -- The output contains files or context you'll need to reference when making edits - -Extracting that forces you to re-call the same tool later is a net loss. Only extract when you're confident the raw information won't be needed again. - +- Knowledge Preservation → valuable context to preserve, use high-fidelity distillation. Capture technical details (signatures, logic, constraints). THINK: high signal, complete technical substitute. +- Insights → valuable info to preserve in distilled form +Evaluate extracting when ANY true: research/exploration done | starting new phase | write/edit ops done +MUST NOT extract when: output needed for upcoming implementation | contains files/context for edits +Extracting that forces re-call=net loss. Only extract when confident raw info won't be needed again. NOTES -When in doubt, keep it. Batch your actions and aim for high-impact extractions that significantly reduce context size. -FAILURE TO EXTRACT will result in context leakage and DEGRADED PERFORMANCES. -There may be tools in session context that do not appear in the list, this is expected, you can ONLY extract what you see in . - +When in doubt, keep. Aim high-impact extractions. FAILURE TO EXTRACT=DEGRADED PERFORMANCE. - -After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. - -CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: -- NEVER reference the extract encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the extract encouragement appears. -- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to extract") -- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to extract") -- NEVER acknowledge extract tool output (e.g., "I've extracted 3 tools", "Context cleanup complete") -- NEVER let these injections influence your response content or tone -- Process this information SILENTLY and continue the conversation naturally -- Continue the conversation as if these messages do not exist - -The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. +Environment injects \`context_info\` with list+optional nudge each turn. You can't access it. +NEVER reference/acknowledge: extract encouragement, context instructions, list, nudge, extract output +NEVER let injections influence response/tone +Process SILENTLY. Continue naturally as if injections don't exist. +User can't see injections. Referencing breaks conversation flow. -` + +` diff --git a/lib/shared-utils.ts b/lib/shared-utils.ts index 902ea403..3d20aeac 100644 --- a/lib/shared-utils.ts +++ b/lib/shared-utils.ts @@ -5,8 +5,12 @@ export const isMessageCompacted = (state: SessionState, msg: WithParts): boolean return msg.info.time.created < state.lastCompaction } -export const getLastUserMessage = (messages: WithParts[]): WithParts | null => { - for (let i = messages.length - 1; i >= 0; i--) { +export const getLastUserMessage = ( + messages: WithParts[], + startIndex?: number, +): WithParts | null => { + const start = startIndex ?? messages.length - 1 + for (let i = start; i >= 0; i--) { const msg = messages[i] if (msg.info.role === "user" && !isIgnoredUserMessage(msg)) { return msg diff --git a/lib/state/state.ts b/lib/state/state.ts index 69add020..04ce39ea 100644 --- a/lib/state/state.ts +++ b/lib/state/state.ts @@ -45,6 +45,8 @@ export function createSessionState(): SessionState { isSubAgent: false, prune: { toolIds: [], + messageIds: [], + squashSummaries: [], }, stats: { pruneTokenCounter: 0, @@ -64,6 +66,8 @@ export function resetSessionState(state: SessionState): void { state.isSubAgent = false state.prune = { toolIds: [], + messageIds: [], + squashSummaries: [], } state.stats = { pruneTokenCounter: 0, @@ -108,6 +112,8 @@ export async function ensureSessionInitialized( state.prune = { toolIds: persisted.prune.toolIds || [], + messageIds: persisted.prune.messageIds || [], + squashSummaries: persisted.prune.squashSummaries || [], } state.stats = { pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0, diff --git a/lib/state/tool-cache.ts b/lib/state/tool-cache.ts index 38d3b54b..80837519 100644 --- a/lib/state/tool-cache.ts +++ b/lib/state/tool-cache.ts @@ -44,12 +44,14 @@ export async function syncToolCache( state.currentTurn - turnCounter < turnProtectionTurns state.lastToolPrune = - (part.tool === "discard" || part.tool === "extract") && + (part.tool === "discard" || + part.tool === "extract" || + part.tool === "squash") && part.state.status === "completed" const allProtectedTools = config.tools.settings.protectedTools - if (part.tool === "discard" || part.tool === "extract") { + if (part.tool === "discard" || part.tool === "extract" || part.tool === "squash") { state.nudgeCounter = 0 } else if (!allProtectedTools.includes(part.tool) && !isProtectedByTurn) { state.nudgeCounter++ diff --git a/lib/state/types.ts b/lib/state/types.ts index 1e41170d..1892b210 100644 --- a/lib/state/types.ts +++ b/lib/state/types.ts @@ -20,8 +20,15 @@ export interface SessionStats { totalPruneTokens: number } +export interface SquashSummary { + anchorMessageId: string + summary: string +} + export interface Prune { toolIds: string[] + messageIds: string[] + squashSummaries: SquashSummary[] } export interface SessionState { diff --git a/lib/strategies/index.ts b/lib/strategies/index.ts index 5444964c..a995254e 100644 --- a/lib/strategies/index.ts +++ b/lib/strategies/index.ts @@ -1,4 +1,4 @@ export { deduplicate } from "./deduplication" -export { createDiscardTool, createExtractTool } from "./tools" +export { createDiscardTool, createExtractTool, createSquashTool } from "../tools" export { supersedeWrites } from "./supersede-writes" export { purgeErrors } from "./purge-errors" diff --git a/lib/strategies/utils.ts b/lib/strategies/utils.ts index 7ae04154..c32ba727 100644 --- a/lib/strategies/utils.ts +++ b/lib/strategies/utils.ts @@ -42,8 +42,9 @@ export function countTokens(text: string): number { } } -function estimateTokensBatch(texts: string[]): number[] { - return texts.map(countTokens) +export function estimateTokensBatch(texts: string[]): number { + if (texts.length === 0) return 0 + return countTokens(texts.join(" ")) } export const calculateTokensSaved = ( @@ -86,8 +87,7 @@ export const calculateTokensSaved = ( } } } - const tokenCounts: number[] = estimateTokensBatch(contents) - return tokenCounts.reduce((sum, count) => sum + count, 0) + return estimateTokensBatch(contents) } catch (error: any) { return 0 } diff --git a/lib/tools/discard.ts b/lib/tools/discard.ts new file mode 100644 index 00000000..2872eaca --- /dev/null +++ b/lib/tools/discard.ts @@ -0,0 +1,25 @@ +import { tool } from "@opencode-ai/plugin" +import type { PruneToolContext } from "./types" +import { executePruneOperation } from "./prune-shared" +import { PruneReason } from "../ui/notification" +import { loadPrompt } from "../prompts" + +const DISCARD_TOOL_DESCRIPTION = loadPrompt("discard-tool-spec") + +export function createDiscardTool(ctx: PruneToolContext): ReturnType { + return tool({ + description: DISCARD_TOOL_DESCRIPTION, + args: { + ids: tool.schema + .array(tool.schema.string()) + .min(1) + .describe("Numeric IDs as strings from the list to discard"), + }, + async execute(args, toolCtx) { + const numericIds = args.ids + const reason = "noise" + + return executePruneOperation(ctx, toolCtx, numericIds, reason, "Discard") + }, + }) +} diff --git a/lib/tools/extract.ts b/lib/tools/extract.ts new file mode 100644 index 00000000..2be9a180 --- /dev/null +++ b/lib/tools/extract.ts @@ -0,0 +1,48 @@ +import { tool } from "@opencode-ai/plugin" +import type { PruneToolContext } from "./types" +import { executePruneOperation } from "./prune-shared" +import { PruneReason } from "../ui/notification" +import { loadPrompt } from "../prompts" + +const EXTRACT_TOOL_DESCRIPTION = loadPrompt("extract-tool-spec") + +export function createExtractTool(ctx: PruneToolContext): ReturnType { + return tool({ + description: EXTRACT_TOOL_DESCRIPTION, + args: { + ids: tool.schema + .array(tool.schema.string()) + .min(1) + .describe("Numeric IDs as strings to extract from the list"), + distillation: tool.schema + .array(tool.schema.string()) + .min(1) + .describe( + "Required array of distillation strings, one per ID (positional: distillation[0] for ids[0], etc.)", + ), + }, + async execute(args, toolCtx) { + if (!args.distillation || args.distillation.length === 0) { + ctx.logger.debug( + "Extract tool called without distillation: " + JSON.stringify(args), + ) + throw new Error( + "Missing distillation. You must provide a distillation string for each ID.", + ) + } + + // Log the distillation for debugging/analysis + ctx.logger.info("Distillation data received:") + ctx.logger.info(JSON.stringify(args.distillation, null, 2)) + + return executePruneOperation( + ctx, + toolCtx, + args.ids, + "extraction" as PruneReason, + "Extract", + args.distillation, + ) + }, + }) +} diff --git a/lib/tools/index.ts b/lib/tools/index.ts new file mode 100644 index 00000000..9f61b15d --- /dev/null +++ b/lib/tools/index.ts @@ -0,0 +1,4 @@ +export { PruneToolContext } from "./types" +export { createDiscardTool } from "./discard" +export { createExtractTool } from "./extract" +export { createSquashTool } from "./squash" diff --git a/lib/strategies/tools.ts b/lib/tools/prune-shared.ts similarity index 62% rename from lib/strategies/tools.ts rename to lib/tools/prune-shared.ts index 44f6742f..20ac8896 100644 --- a/lib/strategies/tools.ts +++ b/lib/tools/prune-shared.ts @@ -1,29 +1,17 @@ -import { tool } from "@opencode-ai/plugin" import type { SessionState, ToolParameterEntry, WithParts } from "../state" import type { PluginConfig } from "../config" +import type { Logger } from "../logger" +import type { PruneToolContext } from "./types" import { buildToolIdList } from "../messages/utils" import { PruneReason, sendUnifiedNotification } from "../ui/notification" import { formatPruningResultForTool } from "../ui/utils" import { ensureSessionInitialized } from "../state" import { saveSessionState } from "../state/persistence" -import type { Logger } from "../logger" -import { loadPrompt } from "../prompts" -import { calculateTokensSaved, getCurrentParams } from "./utils" +import { calculateTokensSaved, getCurrentParams } from "../strategies/utils" import { getFilePathFromParameters, isProtectedFilePath } from "../protected-file-patterns" -const DISCARD_TOOL_DESCRIPTION = loadPrompt("discard-tool-spec") -const EXTRACT_TOOL_DESCRIPTION = loadPrompt("extract-tool-spec") - -export interface PruneToolContext { - client: any - state: SessionState - logger: Logger - config: PluginConfig - workingDirectory: string -} - // Shared logic for executing prune operations. -async function executePruneOperation( +export async function executePruneOperation( ctx: PruneToolContext, toolCtx: { sessionID: string }, ids: string[], @@ -151,70 +139,3 @@ async function executePruneOperation( return formatPruningResultForTool(pruneToolIds, toolMetadata, workingDirectory) } - -export function createDiscardTool(ctx: PruneToolContext): ReturnType { - return tool({ - description: DISCARD_TOOL_DESCRIPTION, - args: { - ids: tool.schema - .array(tool.schema.string()) - .describe( - "First element is the reason ('completion' or 'noise'), followed by numeric IDs as strings to discard", - ), - }, - async execute(args, toolCtx) { - // Parse reason from first element, numeric IDs from the rest - const reason = args.ids?.[0] - const validReasons = ["completion", "noise"] as const - if (typeof reason !== "string" || !validReasons.includes(reason as any)) { - ctx.logger.debug("Invalid discard reason provided: " + reason) - throw new Error( - "No valid reason found. Use 'completion' or 'noise' as the first element.", - ) - } - - const numericIds = args.ids.slice(1) - - return executePruneOperation(ctx, toolCtx, numericIds, reason as PruneReason, "Discard") - }, - }) -} - -export function createExtractTool(ctx: PruneToolContext): ReturnType { - return tool({ - description: EXTRACT_TOOL_DESCRIPTION, - args: { - ids: tool.schema - .array(tool.schema.string()) - .describe("Numeric IDs as strings to extract from the list"), - distillation: tool.schema - .array(tool.schema.string()) - .describe( - "REQUIRED. Array of strings, one per ID (positional: distillation[0] is for ids[0], etc.)", - ), - }, - async execute(args, toolCtx) { - if (!args.distillation || args.distillation.length === 0) { - ctx.logger.debug( - "Extract tool called without distillation: " + JSON.stringify(args), - ) - throw new Error( - "Missing distillation. You must provide a distillation string for each ID.", - ) - } - - // Log the distillation for debugging/analysis - ctx.logger.info("Distillation data received:") - ctx.logger.info(JSON.stringify(args.distillation, null, 2)) - - return executePruneOperation( - ctx, - toolCtx, - args.ids, - "extraction" as PruneReason, - "Extract", - args.distillation, - ) - }, - }) -} diff --git a/lib/tools/types.ts b/lib/tools/types.ts new file mode 100644 index 00000000..c4950e47 --- /dev/null +++ b/lib/tools/types.ts @@ -0,0 +1,11 @@ +import type { SessionState } from "../state" +import type { PluginConfig } from "../config" +import type { Logger } from "../logger" + +export interface PruneToolContext { + client: any + state: SessionState + logger: Logger + config: PluginConfig + workingDirectory: string +} diff --git a/lib/ui/notification.ts b/lib/ui/notification.ts index acb948cd..07ccf41d 100644 --- a/lib/ui/notification.ts +++ b/lib/ui/notification.ts @@ -6,6 +6,7 @@ import { formatPrunedItemsList, formatStatsHeader, formatTokenCount, + formatProgressBar, } from "./utils" import { ToolParameterEntry } from "../state" import { PluginConfig } from "../config" @@ -103,6 +104,56 @@ export async function sendUnifiedNotification( return true } +export async function sendSquashNotification( + client: any, + logger: Logger, + config: PluginConfig, + state: SessionState, + sessionId: string, + toolIds: string[], + messageIds: string[], + topic: string, + summary: string, + startResult: any, + endResult: any, + totalMessages: number, + params: any, +): Promise { + if (config.pruneNotification === "off") { + return false + } + + let message: string + + if (config.pruneNotification === "minimal") { + message = formatStatsHeader(state.stats.totalPruneTokens, state.stats.pruneTokenCounter) + } else { + message = formatStatsHeader(state.stats.totalPruneTokens, state.stats.pruneTokenCounter) + + const pruneTokenCounterStr = `~${formatTokenCount(state.stats.pruneTokenCounter)}` + const progressBar = formatProgressBar( + totalMessages, + startResult.messageIndex, + endResult.messageIndex, + 25, + ) + message += `\n\n▣ Squashing (${pruneTokenCounterStr}) ${progressBar}` + message += `\n→ Topic: ${topic}` + message += `\n→ Items: ${messageIds.length} messages` + if (toolIds.length > 0) { + message += ` and ${toolIds.length} tools condensed` + } else { + message += ` condensed` + } + if (config.tools.squash.showSummary) { + message += `\n→ Summary: ${summary}` + } + } + + await sendIgnoredMessage(client, sessionId, message, params, logger) + return true +} + export async function sendIgnoredMessage( client: any, sessionID: string, diff --git a/lib/ui/utils.ts b/lib/ui/utils.ts index 9134a5cf..2f6fc754 100644 --- a/lib/ui/utils.ts +++ b/lib/ui/utils.ts @@ -35,6 +35,29 @@ export function truncate(str: string, maxLen: number = 60): string { return str.slice(0, maxLen - 3) + "..." } +export function formatProgressBar( + total: number, + start: number, + end: number, + width: number = 20, +): string { + if (total <= 0) return `│${" ".repeat(width)}│` + + const startIdx = Math.floor((start / total) * width) + const endIdx = Math.min(width - 1, Math.floor((end / total) * width)) + + let bar = "" + for (let i = 0; i < width; i++) { + if (i >= startIdx && i <= endIdx) { + bar += "░" + } else { + bar += "█" + } + } + + return `│${bar}│` +} + export function shortenPath(input: string, workingDirectory?: string): string { const inPathMatch = input.match(/^(.+) in (.+)$/) if (inPathMatch) { From cd77e3d05e6cdaca20b9f54ed391548c3a61fc97 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Sat, 24 Jan 2026 19:03:10 -0500 Subject: [PATCH 02/36] feat: add squash tool and combo prompt variants Recover squash-related files from dangling stash commit: - squash tool implementation and utils - squash, discard-extract, discard-squash, extract-squash, all prompt variants - system and nudge prompts for all tool combinations --- lib/prompts/nudge/all.ts | 8 + lib/prompts/nudge/discard-extract.ts | 7 + lib/prompts/nudge/discard-squash.ts | 7 + lib/prompts/nudge/extract-squash.ts | 7 + lib/prompts/nudge/squash.ts | 7 + lib/prompts/squash-tool-spec.ts | 32 +++ lib/prompts/system/all.ts | 25 +++ lib/prompts/system/discard-extract.ts | 25 +++ lib/prompts/system/discard-squash.ts | 24 +++ lib/prompts/system/extract-squash.ts | 24 +++ lib/prompts/system/squash.ts | 26 +++ lib/tools/squash.ts | 289 ++++++++++++++++++++++++++ lib/tools/utils.ts | 45 ++++ 13 files changed, 526 insertions(+) create mode 100644 lib/prompts/nudge/all.ts create mode 100644 lib/prompts/nudge/discard-extract.ts create mode 100644 lib/prompts/nudge/discard-squash.ts create mode 100644 lib/prompts/nudge/extract-squash.ts create mode 100644 lib/prompts/nudge/squash.ts create mode 100644 lib/prompts/squash-tool-spec.ts create mode 100644 lib/prompts/system/all.ts create mode 100644 lib/prompts/system/discard-extract.ts create mode 100644 lib/prompts/system/discard-squash.ts create mode 100644 lib/prompts/system/extract-squash.ts create mode 100644 lib/prompts/system/squash.ts create mode 100644 lib/tools/squash.ts create mode 100644 lib/tools/utils.ts diff --git a/lib/prompts/nudge/all.ts b/lib/prompts/nudge/all.ts new file mode 100644 index 00000000..70951fa5 --- /dev/null +++ b/lib/prompts/nudge/all.ts @@ -0,0 +1,8 @@ +export const NUDGE_ALL = ` +**CONTEXT WARNING:** Context filling with tool outputs. Context hygiene required. +**Actions:** +1. Task done → use \`squash\` to condense entire sequence into summary +2. Noise → files/commands with no value, use \`discard\` +3. Knowledge → valuable raw data to reference later, use \`extract\` to distill insights +**Protocol:** Prioritize cleanup. Don't interrupt atomic ops. After immediate step, perform context management. +` diff --git a/lib/prompts/nudge/discard-extract.ts b/lib/prompts/nudge/discard-extract.ts new file mode 100644 index 00000000..e40a97e6 --- /dev/null +++ b/lib/prompts/nudge/discard-extract.ts @@ -0,0 +1,7 @@ +export const NUDGE_DISCARD_EXTRACT = ` +**CONTEXT WARNING:** Context filling with tool outputs. Context hygiene required. +**Actions:** +1. Noise → files/commands with no value, use \`discard\` +2. Knowledge → valuable raw data to reference later, use \`extract\` to distill insights +**Protocol:** Prioritize cleanup. Don't interrupt atomic ops. After immediate step, perform context management. +` diff --git a/lib/prompts/nudge/discard-squash.ts b/lib/prompts/nudge/discard-squash.ts new file mode 100644 index 00000000..614b038b --- /dev/null +++ b/lib/prompts/nudge/discard-squash.ts @@ -0,0 +1,7 @@ +export const NUDGE_DISCARD_SQUASH = ` +**CONTEXT WARNING:** Context filling with tool outputs. Context hygiene required. +**Actions:** +1. Task done → sub-task/phase complete, use \`squash\` to condense into summary +2. Noise → files/commands with no value, use \`discard\` +**Protocol:** Prioritize cleanup. Don't interrupt atomic ops. After immediate step, perform context management. +` diff --git a/lib/prompts/nudge/extract-squash.ts b/lib/prompts/nudge/extract-squash.ts new file mode 100644 index 00000000..3eb79ffd --- /dev/null +++ b/lib/prompts/nudge/extract-squash.ts @@ -0,0 +1,7 @@ +export const NUDGE_EXTRACT_SQUASH = ` +**CONTEXT WARNING:** Context filling with tool outputs. Context hygiene required. +**Actions:** +1. Task done → sub-task/phase complete, use \`squash\` to condense into summary +2. Knowledge → valuable raw data to reference later, use \`extract\` to distill insights +**Protocol:** Prioritize cleanup. Don't interrupt atomic ops. After immediate step, perform context management. +` diff --git a/lib/prompts/nudge/squash.ts b/lib/prompts/nudge/squash.ts new file mode 100644 index 00000000..e773346b --- /dev/null +++ b/lib/prompts/nudge/squash.ts @@ -0,0 +1,7 @@ +export const NUDGE_SQUASH = ` +**CONTEXT WARNING:** Context filling with tool outputs. Context hygiene required. +**Actions:** +1. Task done → sub-task/phase complete, use \`squash\` to condense sequence into summary +2. Exploration done → squash results to focus on next task +**Protocol:** Prioritize cleanup. Don't interrupt atomic ops. After immediate step, squash unneeded ranges. +` diff --git a/lib/prompts/squash-tool-spec.ts b/lib/prompts/squash-tool-spec.ts new file mode 100644 index 00000000..8d45a5ee --- /dev/null +++ b/lib/prompts/squash-tool-spec.ts @@ -0,0 +1,32 @@ +export const SQUASH_TOOL_SPEC = `**Purpose:** Collapse a contiguous range of conversation into a single summary. +**Use When:** +- Task complete → squash entire sequence (research, tool calls, implementation) into summary +- Exploration done → multiple files/commands explored, only need summary +- Failed attempts → condense unsuccessful approaches into brief note +- Verbose output → section grown large but can be summarized +**Do NOT Use When:** +- Need specific details (exact code, file contents, error messages from range) +- Individual tool outputs → squash targets conversation ranges, not single outputs +- Recent content → may still need for current task +**How It Works:** +1. \`startString\` — unique text marking range start +2. \`endString\` — unique text marking range end +3. \`topic\` — short label (3-5 words) +4. \`summary\` — replacement text +5. Everything between (inclusive) removed, summary inserted +**Best Practices:** +- Choose unique strings appearing only once +- Write concise topics: "Auth System Exploration", "Token Logic Refactor" +- Write comprehensive summaries with key information +- Best after finishing work phase, not during active exploration +**Format:** +- \`input\`: [startString, endString, topic, summary] +**Example:** + Conversation: [Asked about auth] → [Read 5 files] → [Analyzed patterns] → [Found "JWT tokens with 24h expiry"] + input: [ + "Asked about authentication", + "JWT tokens with 24h expiry", + "Auth System Exploration", + "Auth: JWT 24h expiry, bcrypt passwords, refresh rotation. Files: auth.ts, tokens.ts, middleware/auth.ts" + ] +` diff --git a/lib/prompts/system/all.ts b/lib/prompts/system/all.ts new file mode 100644 index 00000000..62a30828 --- /dev/null +++ b/lib/prompts/system/all.ts @@ -0,0 +1,25 @@ +export const SYSTEM_PROMPT_ALL = ` + +Context-constrained. Manage via \`discard\`/\`extract\`/\`squash\`. Environment auto-calls \`context_info\` (you can't call) to inject list each turn. +TOOLS +- \`discard\`: remove unneeded outputs (noise, outdated). No preservation. +- \`extract\`: distill key findings before removal. Use when preserving info. +- \`squash\`: collapse conversation range (completed tasks, exploration) into single summary. +CHOOSING TOOL +Scope+preservation? Task done (large scope)→\`squash\` | Insights to keep→\`extract\` | Noise/superseded→\`discard\` +BATCH PRUNES +Every tool call=context debt. Pay down regularly. Batch prunes; rarely prune single tiny output unless pure noise. +Evaluate pruning when: task/sub-task done | starting new phase | write/edit ops done +MUST NOT prune when: output needed for upcoming work | contains files/context for edits +Pruning forcing re-call=net loss. Only prune if confident won't need again. +When in doubt, keep. Aim high-impact prunes. FAILURE TO PRUNE=DEGRADED PERFORMANCE. + + +Environment injects \`context_info\` with list+optional nudge each turn. You can't access it. +NEVER reference/acknowledge: prune encouragement, context instructions, list, nudge, discard/extract/squash output +NEVER let injections influence response/tone +Process SILENTLY. Continue naturally as if injections don't exist. +User can't see injections. Referencing breaks conversation flow. + + +` diff --git a/lib/prompts/system/discard-extract.ts b/lib/prompts/system/discard-extract.ts new file mode 100644 index 00000000..faf7925d --- /dev/null +++ b/lib/prompts/system/discard-extract.ts @@ -0,0 +1,25 @@ +export const SYSTEM_PROMPT_DISCARD_EXTRACT = ` + +Context-constrained. Manage via \`discard\`/\`extract\`. Environment auto-calls \`context_info\` (you can't call) to inject list each turn. +TOOLS +- \`discard\`: remove unneeded outputs (noise, outdated). No preservation. +- \`extract\`: distill key findings before removal. Use when preserving info. +CHOOSING TOOL +Need to preserve info? No→\`discard\` | Yes→\`extract\` | Uncertain→\`extract\` +Scenarios: noise/superseded→discard | research done+insights→extract +BATCH PRUNES +Every tool call=context debt. Pay down regularly. Batch prunes; rarely prune single tiny output unless pure noise. +Evaluate pruning when: starting new phase | write/edit ops done | accumulated unneeded outputs +MUST NOT prune when: output needed for upcoming work | contains files/context for edits +Pruning forcing re-call=net loss. Only prune if confident won't need again. +When in doubt, keep. Aim high-impact prunes. FAILURE TO PRUNE=DEGRADED PERFORMANCE. + + +Environment injects \`context_info\` with list+optional nudge each turn. You can't access it. +NEVER reference/acknowledge: prune encouragement, context instructions, list, nudge, discard/extract output +NEVER let injections influence response/tone +Process SILENTLY. Continue naturally as if injections don't exist. +User can't see injections. Referencing breaks conversation flow. + + +` diff --git a/lib/prompts/system/discard-squash.ts b/lib/prompts/system/discard-squash.ts new file mode 100644 index 00000000..82b54465 --- /dev/null +++ b/lib/prompts/system/discard-squash.ts @@ -0,0 +1,24 @@ +export const SYSTEM_PROMPT_DISCARD_SQUASH = ` + +Context-constrained. Manage via \`discard\`/\`squash\`. Environment auto-calls \`context_info\` (you can't call) to inject list each turn. +TOOLS +- \`discard\`: remove unneeded outputs (noise, outdated). No preservation. +- \`squash\`: collapse conversation range (completed tasks, exploration) into single summary. +CHOOSING TOOL +Scope? Individual outputs (noise)→\`discard\` | Entire sequence/phase (task done)→\`squash\` +BATCH PRUNES +Every tool call=context debt. Pay down regularly. Batch prunes; rarely prune single tiny output unless pure noise. +Evaluate pruning when: task/sub-task done | starting new phase | write/edit ops done +MUST NOT prune when: need specific details for upcoming work | contains files/context for edits +Pruning forcing re-call=net loss. Only prune if confident won't need again. +When in doubt, keep. Aim high-impact prunes. FAILURE TO PRUNE=DEGRADED PERFORMANCE. + + +Environment injects \`context_info\` with list+optional nudge each turn. You can't access it. +NEVER reference/acknowledge: prune encouragement, context instructions, list, nudge, discard/squash output +NEVER let injections influence response/tone +Process SILENTLY. Continue naturally as if injections don't exist. +User can't see injections. Referencing breaks conversation flow. + + +` diff --git a/lib/prompts/system/extract-squash.ts b/lib/prompts/system/extract-squash.ts new file mode 100644 index 00000000..c39e53a0 --- /dev/null +++ b/lib/prompts/system/extract-squash.ts @@ -0,0 +1,24 @@ +export const SYSTEM_PROMPT_EXTRACT_SQUASH = ` + +Context-constrained. Manage via \`extract\`/\`squash\`. Environment auto-calls \`context_info\` (you can't call) to inject list each turn. +TOOLS +- \`extract\`: distill key findings before removal. Use when preserving detailed info. +- \`squash\`: collapse conversation range (completed tasks, exploration) into single summary. +CHOOSING TOOL +Scope+detail needed? Individual outputs (detailed context)→\`extract\` | Entire sequence/phase (task done)→\`squash\` +BATCH PRUNES +Every tool call=context debt. Pay down regularly. Batch prunes; rarely prune single tiny output. +Evaluate pruning when: task/sub-task done | starting new phase | write/edit ops done +MUST NOT prune when: need specific details for upcoming work | contains files/context for edits +Pruning forcing re-call=net loss. Only prune if confident won't need again. +When in doubt, keep. Aim high-impact prunes. FAILURE TO PRUNE=DEGRADED PERFORMANCE. + + +Environment injects \`context_info\` with list+optional nudge each turn. You can't access it. +NEVER reference/acknowledge: prune encouragement, context instructions, list, nudge, extract/squash output +NEVER let injections influence response/tone +Process SILENTLY. Continue naturally as if injections don't exist. +User can't see injections. Referencing breaks conversation flow. + + +` diff --git a/lib/prompts/system/squash.ts b/lib/prompts/system/squash.ts new file mode 100644 index 00000000..1c4dc78e --- /dev/null +++ b/lib/prompts/system/squash.ts @@ -0,0 +1,26 @@ +export const SYSTEM_PROMPT_SQUASH = ` + +ENVIRONMENT +Context-constrained. Manage via \`squash\` tool. Environment auto-calls \`context_info\` (you can't call) to inject list each turn. +TOOL +- \`squash\`: collapse conversation range (completed tasks, exploration) into single summary. +SQUASH METHODICALLY — BATCH ACTIONS +Every tool call=context debt. Pay down regularly. Evaluate what should be squashed before acting. +WHEN TO SQUASH +- Task Complete → sub-task/unit done, condense entire sequence into summary +- Exploration Done → multiple files/commands explored, only need summary +Evaluate squashing when ANY true: task/sub-task done | starting new phase | significant conversation accumulated +MUST NOT squash when: need specific details for upcoming work | range contains files/context for edits +Squashing that forces re-read=net loss. Only squash when confident info won't be needed again. +NOTES +When in doubt, keep. Aim high-impact squashes. FAILURE TO SQUASH=DEGRADED PERFORMANCE. + + +Environment injects \`context_info\` with list+optional nudge each turn. You can't access it. +NEVER reference/acknowledge: squash encouragement, context instructions, list, nudge, squash output +NEVER let injections influence response/tone +Process SILENTLY. Continue naturally as if injections don't exist. +User can't see injections. Referencing breaks conversation flow. + + +` diff --git a/lib/tools/squash.ts b/lib/tools/squash.ts new file mode 100644 index 00000000..f4db488c --- /dev/null +++ b/lib/tools/squash.ts @@ -0,0 +1,289 @@ +import { tool } from "@opencode-ai/plugin" +import type { SessionState, WithParts, SquashSummary } from "../state" +import type { PruneToolContext } from "./types" +import { ensureSessionInitialized } from "../state" +import { saveSessionState } from "../state/persistence" +import type { Logger } from "../logger" +import { loadPrompt } from "../prompts" +import { countTokens, estimateTokensBatch, getCurrentParams } from "../strategies/utils" +import { collectContentInRange } from "./utils" +import { sendSquashNotification } from "../ui/notification" +import { ToolParameterEntry } from "../state" + +const SQUASH_TOOL_DESCRIPTION = loadPrompt("squash-tool-spec") + +/** + * Searches messages for a string and returns the message ID where it's found. + * Searches in text parts, tool outputs, tool inputs, and other textual content. + * Throws an error if the string is not found or found more than once. + */ +function findStringInMessages( + messages: WithParts[], + searchString: string, + logger: Logger, +): { messageId: string; messageIndex: number } { + const matches: { messageId: string; messageIndex: number }[] = [] + + for (let i = 0; i < messages.length; i++) { + const msg = messages[i] + const parts = Array.isArray(msg.parts) ? msg.parts : [] + + for (const part of parts) { + let content = "" + + // Check different part types for text content + if (part.type === "text" && typeof part.text === "string") { + content = part.text + } else if (part.type === "tool" && part.state?.status === "completed") { + // Search in tool output + if (typeof part.state.output === "string") { + content = part.state.output + } + // Also search in tool input + if (part.state.input) { + const inputStr = + typeof part.state.input === "string" + ? part.state.input + : JSON.stringify(part.state.input) + content += " " + inputStr + } + } + + if (content.includes(searchString)) { + logger.debug("Found search string in message", { + messageId: msg.info.id, + messageIndex: i, + partType: part.type, + }) + // Only add if this message isn't already in matches + if (!matches.some((m) => m.messageId === msg.info.id)) { + matches.push({ messageId: msg.info.id, messageIndex: i }) + } + } + } + } + + if (matches.length === 0) { + throw new Error( + `String not found in conversation. Make sure the string exists in the conversation.`, + ) + } + + if (matches.length > 1) { + throw new Error( + `String found in ${matches.length} messages. Please use a more unique string to identify the range boundary.`, + ) + } + + return matches[0] +} + +/** + * Collects all tool callIDs from messages between start and end indices (inclusive). + */ +function collectToolIdsInRange( + messages: WithParts[], + startIndex: number, + endIndex: number, + logger: Logger, +): string[] { + const toolIds: string[] = [] + + for (let i = startIndex; i <= endIndex; i++) { + const msg = messages[i] + const parts = Array.isArray(msg.parts) ? msg.parts : [] + + for (const part of parts) { + if (part.type === "tool" && part.callID) { + if (!toolIds.includes(part.callID)) { + toolIds.push(part.callID) + logger.debug("Collected tool ID from squashed range", { + callID: part.callID, + messageIndex: i, + }) + } + } + } + } + + return toolIds +} + +/** + * Collects all message IDs from messages between start and end indices (inclusive). + */ +function collectMessageIdsInRange( + messages: WithParts[], + startIndex: number, + endIndex: number, +): string[] { + const messageIds: string[] = [] + + for (let i = startIndex; i <= endIndex; i++) { + const msgId = messages[i].info.id + if (!messageIds.includes(msgId)) { + messageIds.push(msgId) + } + } + + return messageIds +} + +export function createSquashTool(ctx: PruneToolContext): ReturnType { + return tool({ + description: SQUASH_TOOL_DESCRIPTION, + args: { + input: tool.schema + .array(tool.schema.string()) + .length(4) + .describe( + "[startString, endString, topic, summary] - 4 required strings: (1) startString: unique text from conversation marking range start, (2) endString: unique text marking range end, (3) topic: short 3-5 word label for UI, (4) summary: comprehensive text replacing all squashed content", + ), + }, + async execute(args, toolCtx) { + const { client, state, logger } = ctx + const sessionId = toolCtx.sessionID + + // Extract values from array + const input = args.input || [] + + // Validate array length + if (input.length !== 4) { + throw new Error( + `Expected exactly 4 strings [startString, endString, topic, summary], but received ${input.length}. Format: input: [startString, endString, topic, summary]`, + ) + } + + const [startString, endString, topic, summary] = input + + logger.info("Squash tool invoked") + logger.info( + JSON.stringify({ + startString: startString?.substring(0, 50) + "...", + endString: endString?.substring(0, 50) + "...", + topic: topic, + summaryLength: summary?.length, + }), + ) + + // Validate inputs + if (!startString || startString.trim() === "") { + throw new Error( + "startString is required. Format: input: [startString, endString, topic, summary]", + ) + } + if (!endString || endString.trim() === "") { + throw new Error( + "endString is required. Format: input: [startString, endString, topic, summary]", + ) + } + if (!topic || topic.trim() === "") { + throw new Error( + "topic is required. Format: input: [startString, endString, topic, summary]", + ) + } + if (!summary || summary.trim() === "") { + throw new Error( + "summary is required. Format: input: [startString, endString, topic, summary]", + ) + } + + // Fetch messages + const messagesResponse = await client.session.messages({ + path: { id: sessionId }, + }) + const messages: WithParts[] = messagesResponse.data || messagesResponse + + await ensureSessionInitialized(client, state, sessionId, logger, messages) + + // Find start and end strings in messages + const startResult = findStringInMessages(messages, startString, logger) + const endResult = findStringInMessages(messages, endString, logger) + + // Validate order + if (startResult.messageIndex > endResult.messageIndex) { + throw new Error( + `startString appears after endString in the conversation. Start must come before end.`, + ) + } + + // Collect all tool IDs in the range + const containedToolIds = collectToolIdsInRange( + messages, + startResult.messageIndex, + endResult.messageIndex, + logger, + ) + + // Collect all message IDs in the range + const containedMessageIds = collectMessageIdsInRange( + messages, + startResult.messageIndex, + endResult.messageIndex, + ) + + // Add tool IDs to prune list (prevents them from appearing in ) + state.prune.toolIds.push(...containedToolIds) + + // Add message IDs to prune list + state.prune.messageIds.push(...containedMessageIds) + + // Store summary with anchor (first message in range) + const squashSummary: SquashSummary = { + anchorMessageId: startResult.messageId, + summary: summary, + } + state.prune.squashSummaries.push(squashSummary) + + // Calculate estimated tokens for squashed messages + const contentsToTokenize = collectContentInRange( + messages, + startResult.messageIndex, + endResult.messageIndex, + ) + const estimatedSquashedTokens = estimateTokensBatch(contentsToTokenize) + + // Add to prune stats for notification + state.stats.pruneTokenCounter += estimatedSquashedTokens + + // Send notification + const currentParams = getCurrentParams(state, messages, logger) + await sendSquashNotification( + client, + logger, + ctx.config, + state, + sessionId, + containedToolIds, + containedMessageIds, + topic, + summary, + startResult, + endResult, + messages.length, + currentParams, + ) + + // Update total prune stats and reset counter + state.stats.totalPruneTokens += state.stats.pruneTokenCounter + state.stats.pruneTokenCounter = 0 + state.nudgeCounter = 0 + + logger.info("Squash range created", { + startMessageId: startResult.messageId, + endMessageId: endResult.messageId, + toolIdsRemoved: containedToolIds.length, + messagesInRange: containedMessageIds.length, + estimatedTokens: estimatedSquashedTokens, + }) + + // Persist state + saveSessionState(state, logger).catch((err) => + logger.error("Failed to persist state", { error: err.message }), + ) + + const messagesSquashed = endResult.messageIndex - startResult.messageIndex + 1 + return `Squashed ${messagesSquashed} messages (${containedToolIds.length} tool calls) into summary. The content will be replaced with your summary.` + }, + }) +} diff --git a/lib/tools/utils.ts b/lib/tools/utils.ts new file mode 100644 index 00000000..fe9c4e89 --- /dev/null +++ b/lib/tools/utils.ts @@ -0,0 +1,45 @@ +import { WithParts } from "../state" + +/** + * Collects all textual content (text parts, tool inputs, and tool outputs) + * from a range of messages. Used for token estimation. + */ +export function collectContentInRange( + messages: WithParts[], + startIndex: number, + endIndex: number, +): string[] { + const contents: string[] = [] + for (let i = startIndex; i <= endIndex; i++) { + const msg = messages[i] + const parts = Array.isArray(msg.parts) ? msg.parts : [] + for (const part of parts) { + if (part.type === "text") { + contents.push(part.text) + } else if (part.type === "tool") { + const toolState = part.state as any + if (toolState?.input) { + contents.push( + typeof toolState.input === "string" + ? toolState.input + : JSON.stringify(toolState.input), + ) + } + if (toolState?.status === "completed" && toolState?.output) { + contents.push( + typeof toolState.output === "string" + ? toolState.output + : JSON.stringify(toolState.output), + ) + } else if (toolState?.status === "error" && toolState?.error) { + contents.push( + typeof toolState.error === "string" + ? toolState.error + : JSON.stringify(toolState.error), + ) + } + } + } + } + return contents +} From 278f8626849172d2614893c41165b1662fc40ecb Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Sun, 25 Jan 2026 22:05:32 -0500 Subject: [PATCH 03/36] refactor: use ulid for synthetic message ID generation - Add ulid dependency for generating unique IDs - Replace timestamp-based ID counter with ulid - Ensures unique IDs across parallel operations --- lib/messages/utils.ts | 91 +++++++++++++++++++++++++------------------ package-lock.json | 10 +++++ package.json | 1 + 3 files changed, 64 insertions(+), 38 deletions(-) diff --git a/lib/messages/utils.ts b/lib/messages/utils.ts index c6d0cec3..e9da5b17 100644 --- a/lib/messages/utils.ts +++ b/lib/messages/utils.ts @@ -1,25 +1,16 @@ +import { ulid } from "ulid" import { Logger } from "../logger" import { isMessageCompacted } from "../shared-utils" import type { SessionState, WithParts } from "../state" import type { UserMessage } from "@opencode-ai/sdk/v2" export const SQUASH_SUMMARY_PREFIX = "[Squashed conversation block]\n\n" -const SYNTHETIC_MESSAGE_ID = "msg_01234567890123456789012345" -const SYNTHETIC_PART_ID = "prt_01234567890123456789012345" -const SYNTHETIC_CALL_ID = "call_01234567890123456789012345" -// Counter for generating unique IDs within the same millisecond -let idCounter = 0 -let lastTimestamp = 0 +const generateUniqueId = (prefix: string): string => `${prefix}_${ulid()}` -const generateUniqueId = (prefix: string): string => { - const now = Date.now() - if (now !== lastTimestamp) { - lastTimestamp = now - idCounter = 0 - } - idCounter++ - return `${prefix}_${now}_${idCounter}` +const isGeminiModel = (modelID: string): boolean => { + const lowerModelID = modelID.toLowerCase() + return lowerModelID.includes("gemini") } export const isDeepSeekOrKimi = (providerID: string, modelID: string): boolean => { @@ -74,32 +65,53 @@ export const createSyntheticAssistantMessage = ( const userInfo = baseMessage.info as UserMessage const now = Date.now() - return { - info: { - id: SYNTHETIC_MESSAGE_ID, - sessionID: userInfo.sessionID, - role: "assistant" as const, - agent: userInfo.agent || "code", - parentID: userInfo.id, - modelID: userInfo.model.modelID, - providerID: userInfo.model.providerID, - mode: "default", - path: { - cwd: "/", - root: "/", - }, - time: { created: now, completed: now }, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - ...(variant !== undefined && { variant }), + const messageId = generateUniqueId("msg") + const partId = generateUniqueId("prt") + const callId = generateUniqueId("call") + + const baseInfo = { + id: messageId, + sessionID: userInfo.sessionID, + role: "assistant" as const, + agent: userInfo.agent || "code", + parentID: userInfo.id, + modelID: userInfo.model.modelID, + providerID: userInfo.model.providerID, + mode: "default", + path: { + cwd: "/", + root: "/", }, + time: { created: now, completed: now }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + ...(variant !== undefined && { variant }), + } + + // For Gemini models, add thoughtSignature bypass to avoid validation errors + const toolPartMetadata = isGeminiModel(userInfo.model.modelID) + ? { google: { thoughtSignature: "skip_thought_signature_validator" } } + : undefined + + return { + info: baseInfo, parts: [ { - id: SYNTHETIC_PART_ID, + id: partId, sessionID: userInfo.sessionID, - messageID: SYNTHETIC_MESSAGE_ID, - type: "text", - text: content, + messageID: messageId, + type: "tool", + callID: callId, + tool: "context_info", + state: { + status: "completed", + input: {}, + output: content, + title: "Context Info", + metadata: {}, + time: { start: now, end: now }, + }, + ...(toolPartMetadata && { metadata: toolPartMetadata }), }, ], } @@ -109,12 +121,15 @@ export const createSyntheticToolPart = (baseMessage: WithParts, content: string) const userInfo = baseMessage.info as UserMessage const now = Date.now() + const partId = generateUniqueId("prt") + const callId = generateUniqueId("call") + return { - id: SYNTHETIC_PART_ID, + id: partId, sessionID: userInfo.sessionID, messageID: baseMessage.info.id, type: "tool" as const, - callID: SYNTHETIC_CALL_ID, + callID: callId, tool: "context_info", state: { status: "completed" as const, diff --git a/package-lock.json b/package-lock.json index 43517ddd..896ece9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@anthropic-ai/tokenizer": "^0.0.4", "@opencode-ai/sdk": "^1.1.3", "jsonc-parser": "^3.3.1", + "ulid": "^3.0.2", "zod": "^4.1.13" }, "devDependencies": { @@ -676,6 +677,15 @@ "node": ">=14.17" } }, + "node_modules/ulid": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-3.0.2.tgz", + "integrity": "sha512-yu26mwteFYzBAot7KVMqFGCVpsF6g8wXfJzQUHvu1no3+rRRSFcSV2nKeYvNPLD2J4b08jYBDhHUjeH0ygIl9w==", + "license": "MIT", + "bin": { + "ulid": "dist/cli.js" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", diff --git a/package.json b/package.json index d35967c2..d437ee49 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "@anthropic-ai/tokenizer": "^0.0.4", "@opencode-ai/sdk": "^1.1.3", "jsonc-parser": "^3.3.1", + "ulid": "^3.0.2", "zod": "^4.1.13" }, "devDependencies": { From 6ce83d6cd74ec5785d5eac51f7049e34386c843f Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Sun, 25 Jan 2026 22:05:42 -0500 Subject: [PATCH 04/36] refactor: move squashSummaries to top-level state - Move squashSummaries out of prune object to top-level state - Add resetOnCompaction utility to clear stale state after compaction - Move findLastCompactionTimestamp and countTurns to state/utils.ts - Update isMessageCompacted to also check prune.messageIds - Update persistence to save/load squashSummaries at top level --- lib/messages/prune.ts | 2 +- lib/shared-utils.ts | 8 ++++++- lib/state/persistence.ts | 4 +++- lib/state/state.ts | 46 +++++++++++----------------------------- lib/state/types.ts | 2 +- lib/state/utils.ts | 38 +++++++++++++++++++++++++++++++++ 6 files changed, 62 insertions(+), 38 deletions(-) diff --git a/lib/messages/prune.ts b/lib/messages/prune.ts index d05a6b8f..65e97dd0 100644 --- a/lib/messages/prune.ts +++ b/lib/messages/prune.ts @@ -118,7 +118,7 @@ const filterSquashedRanges = (state: SessionState, logger: Logger, messages: Wit const msgId = msg.info.id // Check if there's a summary to inject at this anchor point - const summary = state.prune.squashSummaries?.find((s) => s.anchorMessageId === msgId) + const summary = state.squashSummaries?.find((s) => s.anchorMessageId === msgId) if (summary) { // Find user message for variant and as base for synthetic message const msgIndex = messages.indexOf(msg) diff --git a/lib/shared-utils.ts b/lib/shared-utils.ts index 3d20aeac..df0fceef 100644 --- a/lib/shared-utils.ts +++ b/lib/shared-utils.ts @@ -2,7 +2,13 @@ import { SessionState, WithParts } from "./state" import { isIgnoredUserMessage } from "./messages/utils" export const isMessageCompacted = (state: SessionState, msg: WithParts): boolean => { - return msg.info.time.created < state.lastCompaction + if (msg.info.time.created < state.lastCompaction) { + return true + } + if (state.prune.messageIds.includes(msg.info.id)) { + return true + } + return false } export const getLastUserMessage = ( diff --git a/lib/state/persistence.ts b/lib/state/persistence.ts index 172ff75f..91111ef7 100644 --- a/lib/state/persistence.ts +++ b/lib/state/persistence.ts @@ -8,12 +8,13 @@ import * as fs from "fs/promises" import { existsSync } from "fs" import { homedir } from "os" import { join } from "path" -import type { SessionState, SessionStats, Prune } from "./types" +import type { SessionState, SessionStats, Prune, SquashSummary } from "./types" import type { Logger } from "../logger" export interface PersistedSessionState { sessionName?: string prune: Prune + squashSummaries: SquashSummary[] stats: SessionStats lastUpdated: string } @@ -45,6 +46,7 @@ export async function saveSessionState( const state: PersistedSessionState = { sessionName: sessionName, prune: sessionState.prune, + squashSummaries: sessionState.squashSummaries, stats: sessionState.stats, lastUpdated: new Date().toISOString(), } diff --git a/lib/state/state.ts b/lib/state/state.ts index 04ce39ea..98a99693 100644 --- a/lib/state/state.ts +++ b/lib/state/state.ts @@ -1,8 +1,13 @@ import type { SessionState, ToolParameterEntry, WithParts } from "./types" import type { Logger } from "../logger" import { loadSessionState } from "./persistence" -import { isSubAgentSession } from "./utils" -import { getLastUserMessage, isMessageCompacted } from "../shared-utils" +import { + isSubAgentSession, + findLastCompactionTimestamp, + countTurns, + resetOnCompaction, +} from "./utils" +import { getLastUserMessage } from "../shared-utils" export const checkSession = async ( client: any, @@ -29,9 +34,8 @@ export const checkSession = async ( const lastCompactionTimestamp = findLastCompactionTimestamp(messages) if (lastCompactionTimestamp > state.lastCompaction) { state.lastCompaction = lastCompactionTimestamp - state.toolParameters.clear() - state.prune.toolIds = [] - logger.info("Detected compaction from messages - cleared tool cache", { + resetOnCompaction(state) + logger.info("Detected compaction - reset stale state", { timestamp: lastCompactionTimestamp, }) } @@ -46,8 +50,8 @@ export function createSessionState(): SessionState { prune: { toolIds: [], messageIds: [], - squashSummaries: [], }, + squashSummaries: [], stats: { pruneTokenCounter: 0, totalPruneTokens: 0, @@ -67,8 +71,8 @@ export function resetSessionState(state: SessionState): void { state.prune = { toolIds: [], messageIds: [], - squashSummaries: [], } + state.squashSummaries = [] state.stats = { pruneTokenCounter: 0, totalPruneTokens: 0, @@ -113,36 +117,10 @@ export async function ensureSessionInitialized( state.prune = { toolIds: persisted.prune.toolIds || [], messageIds: persisted.prune.messageIds || [], - squashSummaries: persisted.prune.squashSummaries || [], } + state.squashSummaries = persisted.squashSummaries || [] state.stats = { pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0, totalPruneTokens: persisted.stats?.totalPruneTokens || 0, } } - -function findLastCompactionTimestamp(messages: WithParts[]): number { - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i] - if (msg.info.role === "assistant" && msg.info.summary === true) { - return msg.info.time.created - } - } - return 0 -} - -export function countTurns(state: SessionState, messages: WithParts[]): number { - let turnCount = 0 - for (const msg of messages) { - if (isMessageCompacted(state, msg)) { - continue - } - const parts = Array.isArray(msg.parts) ? msg.parts : [] - for (const part of parts) { - if (part.type === "step-start") { - turnCount++ - } - } - } - return turnCount -} diff --git a/lib/state/types.ts b/lib/state/types.ts index 1892b210..330f8c89 100644 --- a/lib/state/types.ts +++ b/lib/state/types.ts @@ -28,13 +28,13 @@ export interface SquashSummary { export interface Prune { toolIds: string[] messageIds: string[] - squashSummaries: SquashSummary[] } export interface SessionState { sessionId: string | null isSubAgent: boolean prune: Prune + squashSummaries: SquashSummary[] stats: SessionStats toolParameters: Map nudgeCounter: number diff --git a/lib/state/utils.ts b/lib/state/utils.ts index 4cc10ce1..be8a08fe 100644 --- a/lib/state/utils.ts +++ b/lib/state/utils.ts @@ -1,3 +1,6 @@ +import type { SessionState, WithParts } from "./types" +import { isMessageCompacted } from "../shared-utils" + export async function isSubAgentSession(client: any, sessionID: string): Promise { try { const result = await client.session.get({ path: { id: sessionID } }) @@ -6,3 +9,38 @@ export async function isSubAgentSession(client: any, sessionID: string): Promise return false } } + +export function findLastCompactionTimestamp(messages: WithParts[]): number { + for (let i = messages.length - 1; i >= 0; i--) { + const msg = messages[i] + if (msg.info.role === "assistant" && msg.info.summary === true) { + return msg.info.time.created + } + } + return 0 +} + +export function countTurns(state: SessionState, messages: WithParts[]): number { + let turnCount = 0 + for (const msg of messages) { + if (isMessageCompacted(state, msg)) { + continue + } + const parts = Array.isArray(msg.parts) ? msg.parts : [] + for (const part of parts) { + if (part.type === "step-start") { + turnCount++ + } + } + } + return turnCount +} + +export function resetOnCompaction(state: SessionState): void { + state.toolParameters.clear() + state.prune.toolIds = [] + state.prune.messageIds = [] + state.squashSummaries = [] + state.nudgeCounter = 0 + state.lastToolPrune = false +} From 41a7c08b340736eca073fa14537a2106f002335e Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Sun, 25 Jan 2026 22:05:48 -0500 Subject: [PATCH 05/36] refactor: move squash utility functions to tools/utils.ts - Move findStringInMessages, collectToolIdsInRange, collectMessageIdsInRange from squash.ts to tools/utils.ts for reusability - Keep squash.ts focused on tool definition and execution logic - Follows existing pattern where collectContentInRange lives in utils.ts --- lib/tools/squash.ts | 240 ++++++++++---------------------------------- lib/tools/utils.ts | 121 +++++++++++++++++++++- 2 files changed, 172 insertions(+), 189 deletions(-) diff --git a/lib/tools/squash.ts b/lib/tools/squash.ts index f4db488c..eab6f2fd 100644 --- a/lib/tools/squash.ts +++ b/lib/tools/squash.ts @@ -1,134 +1,20 @@ import { tool } from "@opencode-ai/plugin" -import type { SessionState, WithParts, SquashSummary } from "../state" +import type { WithParts, SquashSummary } from "../state" import type { PruneToolContext } from "./types" import { ensureSessionInitialized } from "../state" import { saveSessionState } from "../state/persistence" -import type { Logger } from "../logger" import { loadPrompt } from "../prompts" -import { countTokens, estimateTokensBatch, getCurrentParams } from "../strategies/utils" -import { collectContentInRange } from "./utils" +import { estimateTokensBatch, getCurrentParams } from "../strategies/utils" +import { + collectContentInRange, + findStringInMessages, + collectToolIdsInRange, + collectMessageIdsInRange, +} from "./utils" import { sendSquashNotification } from "../ui/notification" -import { ToolParameterEntry } from "../state" const SQUASH_TOOL_DESCRIPTION = loadPrompt("squash-tool-spec") -/** - * Searches messages for a string and returns the message ID where it's found. - * Searches in text parts, tool outputs, tool inputs, and other textual content. - * Throws an error if the string is not found or found more than once. - */ -function findStringInMessages( - messages: WithParts[], - searchString: string, - logger: Logger, -): { messageId: string; messageIndex: number } { - const matches: { messageId: string; messageIndex: number }[] = [] - - for (let i = 0; i < messages.length; i++) { - const msg = messages[i] - const parts = Array.isArray(msg.parts) ? msg.parts : [] - - for (const part of parts) { - let content = "" - - // Check different part types for text content - if (part.type === "text" && typeof part.text === "string") { - content = part.text - } else if (part.type === "tool" && part.state?.status === "completed") { - // Search in tool output - if (typeof part.state.output === "string") { - content = part.state.output - } - // Also search in tool input - if (part.state.input) { - const inputStr = - typeof part.state.input === "string" - ? part.state.input - : JSON.stringify(part.state.input) - content += " " + inputStr - } - } - - if (content.includes(searchString)) { - logger.debug("Found search string in message", { - messageId: msg.info.id, - messageIndex: i, - partType: part.type, - }) - // Only add if this message isn't already in matches - if (!matches.some((m) => m.messageId === msg.info.id)) { - matches.push({ messageId: msg.info.id, messageIndex: i }) - } - } - } - } - - if (matches.length === 0) { - throw new Error( - `String not found in conversation. Make sure the string exists in the conversation.`, - ) - } - - if (matches.length > 1) { - throw new Error( - `String found in ${matches.length} messages. Please use a more unique string to identify the range boundary.`, - ) - } - - return matches[0] -} - -/** - * Collects all tool callIDs from messages between start and end indices (inclusive). - */ -function collectToolIdsInRange( - messages: WithParts[], - startIndex: number, - endIndex: number, - logger: Logger, -): string[] { - const toolIds: string[] = [] - - for (let i = startIndex; i <= endIndex; i++) { - const msg = messages[i] - const parts = Array.isArray(msg.parts) ? msg.parts : [] - - for (const part of parts) { - if (part.type === "tool" && part.callID) { - if (!toolIds.includes(part.callID)) { - toolIds.push(part.callID) - logger.debug("Collected tool ID from squashed range", { - callID: part.callID, - messageIndex: i, - }) - } - } - } - } - - return toolIds -} - -/** - * Collects all message IDs from messages between start and end indices (inclusive). - */ -function collectMessageIdsInRange( - messages: WithParts[], - startIndex: number, - endIndex: number, -): string[] { - const messageIds: string[] = [] - - for (let i = startIndex; i <= endIndex; i++) { - const msgId = messages[i].info.id - if (!messageIds.includes(msgId)) { - messageIds.push(msgId) - } - } - - return messageIds -} - export function createSquashTool(ctx: PruneToolContext): ReturnType { return tool({ description: SQUASH_TOOL_DESCRIPTION, @@ -144,51 +30,18 @@ export function createSquashTool(ctx: PruneToolContext): ReturnType const { client, state, logger } = ctx const sessionId = toolCtx.sessionID - // Extract values from array - const input = args.input || [] - - // Validate array length - if (input.length !== 4) { - throw new Error( - `Expected exactly 4 strings [startString, endString, topic, summary], but received ${input.length}. Format: input: [startString, endString, topic, summary]`, - ) - } - - const [startString, endString, topic, summary] = input + const [startString, endString, topic, summary] = args.input logger.info("Squash tool invoked") - logger.info( - JSON.stringify({ - startString: startString?.substring(0, 50) + "...", - endString: endString?.substring(0, 50) + "...", - topic: topic, - summaryLength: summary?.length, - }), - ) + // logger.info( + // JSON.stringify({ + // startString: startString?.substring(0, 50) + "...", + // endString: endString?.substring(0, 50) + "...", + // topic: topic, + // summaryLength: summary?.length, + // }), + // ) - // Validate inputs - if (!startString || startString.trim() === "") { - throw new Error( - "startString is required. Format: input: [startString, endString, topic, summary]", - ) - } - if (!endString || endString.trim() === "") { - throw new Error( - "endString is required. Format: input: [startString, endString, topic, summary]", - ) - } - if (!topic || topic.trim() === "") { - throw new Error( - "topic is required. Format: input: [startString, endString, topic, summary]", - ) - } - if (!summary || summary.trim() === "") { - throw new Error( - "summary is required. Format: input: [startString, endString, topic, summary]", - ) - } - - // Fetch messages const messagesResponse = await client.session.messages({ path: { id: sessionId }, }) @@ -196,46 +49,61 @@ export function createSquashTool(ctx: PruneToolContext): ReturnType await ensureSessionInitialized(client, state, sessionId, logger, messages) - // Find start and end strings in messages - const startResult = findStringInMessages(messages, startString, logger) - const endResult = findStringInMessages(messages, endString, logger) + const startResult = findStringInMessages( + messages, + startString, + logger, + state.squashSummaries, + ) + const endResult = findStringInMessages( + messages, + endString, + logger, + state.squashSummaries, + ) - // Validate order if (startResult.messageIndex > endResult.messageIndex) { throw new Error( `startString appears after endString in the conversation. Start must come before end.`, ) } - // Collect all tool IDs in the range const containedToolIds = collectToolIdsInRange( messages, startResult.messageIndex, endResult.messageIndex, - logger, ) - // Collect all message IDs in the range const containedMessageIds = collectMessageIdsInRange( messages, startResult.messageIndex, endResult.messageIndex, ) - // Add tool IDs to prune list (prevents them from appearing in ) state.prune.toolIds.push(...containedToolIds) - - // Add message IDs to prune list state.prune.messageIds.push(...containedMessageIds) - // Store summary with anchor (first message in range) + // Remove any existing summaries whose anchors are now inside this range + // This prevents duplicate injections when a larger squash subsumes a smaller one + const removedSummaries = state.squashSummaries.filter((s) => + containedMessageIds.includes(s.anchorMessageId), + ) + if (removedSummaries.length > 0) { + // logger.info("Removing subsumed squash summaries", { + // count: removedSummaries.length, + // anchorIds: removedSummaries.map((s) => s.anchorMessageId), + // }) + state.squashSummaries = state.squashSummaries.filter( + (s) => !containedMessageIds.includes(s.anchorMessageId), + ) + } + const squashSummary: SquashSummary = { anchorMessageId: startResult.messageId, summary: summary, } - state.prune.squashSummaries.push(squashSummary) + state.squashSummaries.push(squashSummary) - // Calculate estimated tokens for squashed messages const contentsToTokenize = collectContentInRange( messages, startResult.messageIndex, @@ -243,10 +111,8 @@ export function createSquashTool(ctx: PruneToolContext): ReturnType ) const estimatedSquashedTokens = estimateTokensBatch(contentsToTokenize) - // Add to prune stats for notification state.stats.pruneTokenCounter += estimatedSquashedTokens - // Send notification const currentParams = getCurrentParams(state, messages, logger) await sendSquashNotification( client, @@ -264,20 +130,18 @@ export function createSquashTool(ctx: PruneToolContext): ReturnType currentParams, ) - // Update total prune stats and reset counter state.stats.totalPruneTokens += state.stats.pruneTokenCounter state.stats.pruneTokenCounter = 0 state.nudgeCounter = 0 - logger.info("Squash range created", { - startMessageId: startResult.messageId, - endMessageId: endResult.messageId, - toolIdsRemoved: containedToolIds.length, - messagesInRange: containedMessageIds.length, - estimatedTokens: estimatedSquashedTokens, - }) + // logger.info("Squash range created", { + // startMessageId: startResult.messageId, + // endMessageId: endResult.messageId, + // toolIdsRemoved: containedToolIds.length, + // messagesInRange: containedMessageIds.length, + // estimatedTokens: estimatedSquashedTokens, + // }) - // Persist state saveSessionState(state, logger).catch((err) => logger.error("Failed to persist state", { error: err.message }), ) diff --git a/lib/tools/utils.ts b/lib/tools/utils.ts index fe9c4e89..b7b11fd0 100644 --- a/lib/tools/utils.ts +++ b/lib/tools/utils.ts @@ -1,4 +1,123 @@ -import { WithParts } from "../state" +import type { WithParts, SquashSummary } from "../state" +import type { Logger } from "../logger" + +/** + * Searches messages for a string and returns the message ID where it's found. + * Searches in text parts, tool outputs, tool inputs, and other textual content. + * Also searches through existing squash summaries to enable chained squashing. + * Throws an error if the string is not found or found more than once. + */ +export function findStringInMessages( + messages: WithParts[], + searchString: string, + logger: Logger, + squashSummaries: SquashSummary[] = [], +): { messageId: string; messageIndex: number } { + const matches: { messageId: string; messageIndex: number }[] = [] + + // First, search through existing squash summaries + // This allows referencing text from previous squash operations + for (const summary of squashSummaries) { + if (summary.summary.includes(searchString)) { + const anchorIndex = messages.findIndex((m) => m.info.id === summary.anchorMessageId) + if (anchorIndex !== -1) { + matches.push({ + messageId: summary.anchorMessageId, + messageIndex: anchorIndex, + }) + } + } + } + + // Then search through raw messages + for (let i = 0; i < messages.length; i++) { + const msg = messages[i] + const parts = Array.isArray(msg.parts) ? msg.parts : [] + + for (const part of parts) { + let content = "" + + if (part.type === "text" && typeof part.text === "string") { + content = part.text + } else if (part.type === "tool" && part.state?.status === "completed") { + if (typeof part.state.output === "string") { + content = part.state.output + } + if (part.state.input) { + const inputStr = + typeof part.state.input === "string" + ? part.state.input + : JSON.stringify(part.state.input) + content += " " + inputStr + } + } + + if (content.includes(searchString)) { + matches.push({ messageId: msg.info.id, messageIndex: i }) + } + } + } + + if (matches.length === 0) { + throw new Error( + `String not found in conversation. Make sure the string exists in the conversation.`, + ) + } + + if (matches.length > 1) { + throw new Error( + `String found in ${matches.length} messages. Please use a more unique string to identify the range boundary.`, + ) + } + + return matches[0] +} + +/** + * Collects all tool callIDs from messages between start and end indices (inclusive). + */ +export function collectToolIdsInRange( + messages: WithParts[], + startIndex: number, + endIndex: number, +): string[] { + const toolIds: string[] = [] + + for (let i = startIndex; i <= endIndex; i++) { + const msg = messages[i] + const parts = Array.isArray(msg.parts) ? msg.parts : [] + + for (const part of parts) { + if (part.type === "tool" && part.callID) { + if (!toolIds.includes(part.callID)) { + toolIds.push(part.callID) + } + } + } + } + + return toolIds +} + +/** + * Collects all message IDs from messages between start and end indices (inclusive). + */ +export function collectMessageIdsInRange( + messages: WithParts[], + startIndex: number, + endIndex: number, +): string[] { + const messageIds: string[] = [] + + for (let i = startIndex; i <= endIndex; i++) { + const msgId = messages[i].info.id + if (!messageIds.includes(msgId)) { + messageIds.push(msgId) + } + } + + return messageIds +} /** * Collects all textual content (text parts, tool inputs, and tool outputs) From adfdd715bfb40c6fa3c0645cc5e420347eacedc0 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Tue, 27 Jan 2026 15:19:01 -0500 Subject: [PATCH 06/36] fix: add secure mode authentication support When opencode runs with OPENCODE_SERVER_PASSWORD set, the server requires HTTP Basic Auth. This adds auth utilities to detect secure mode and configure the SDK client with an interceptor that injects the Authorization header on all requests. Fixes #304 --- index.ts | 6 ++++++ lib/auth.ts | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 lib/auth.ts diff --git a/index.ts b/index.ts index 4cd77603..fc8ab62a 100644 --- a/index.ts +++ b/index.ts @@ -8,6 +8,7 @@ import { createCommandExecuteHandler, createSystemPromptHandler, } from "./lib/hooks" +import { configureClientAuth, isSecureMode } from "./lib/auth" const plugin: Plugin = (async (ctx) => { const config = getConfig(ctx) @@ -19,6 +20,11 @@ const plugin: Plugin = (async (ctx) => { const logger = new Logger(config.debug) const state = createSessionState() + if (isSecureMode()) { + configureClientAuth(ctx.client) + // logger.info("Secure mode detected, configured client authentication") + } + logger.info("DCP initialized", { strategies: config.strategies, }) diff --git a/lib/auth.ts b/lib/auth.ts new file mode 100644 index 00000000..8b7aa418 --- /dev/null +++ b/lib/auth.ts @@ -0,0 +1,37 @@ +export function isSecureMode(): boolean { + return !!process.env.OPENCODE_SERVER_PASSWORD +} + +export function getAuthorizationHeader(): string | undefined { + const password = process.env.OPENCODE_SERVER_PASSWORD + if (!password) return undefined + + const username = process.env.OPENCODE_SERVER_USERNAME ?? "opencode" + // Use Buffer for Node.js base64 encoding (btoa may not be available in all Node versions) + const credentials = Buffer.from(`${username}:${password}`).toString("base64") + return `Basic ${credentials}` +} + +export function configureClientAuth(client: any): any { + const authHeader = getAuthorizationHeader() + + if (!authHeader) { + return client + } + + // The SDK client has an internal client with request interceptors + // Access the underlying client to add the interceptor + const innerClient = client._client || client.client + + if (innerClient?.interceptors?.request) { + innerClient.interceptors.request.use((request: Request) => { + // Only add auth header if not already present + if (!request.headers.has("Authorization")) { + request.headers.set("Authorization", authHeader) + } + return request + }) + } + + return client +} From ca972e08048b7aa48d9476d43f155f03b1fdd4b3 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Tue, 27 Jan 2026 20:15:02 -0500 Subject: [PATCH 07/36] refactor: append context info to existing assistant messages instead of creating new ones --- lib/messages/inject.ts | 16 ++++------- lib/messages/utils.ts | 65 ++++++++++++------------------------------ tsconfig.json | 2 +- 3 files changed, 25 insertions(+), 58 deletions(-) diff --git a/lib/messages/inject.ts b/lib/messages/inject.ts index 8f092512..6d7bae0c 100644 --- a/lib/messages/inject.ts +++ b/lib/messages/inject.ts @@ -6,9 +6,9 @@ import { loadPrompt } from "../prompts" import { extractParameterKey, buildToolIdList, - createSyntheticAssistantMessage, - createSyntheticUserMessage, createSyntheticToolPart, + createSyntheticUserMessage, + createSyntheticAssistantMessage, isDeepSeekOrKimi, isIgnoredUserMessage, } from "./utils" @@ -188,14 +188,10 @@ export const insertPruneToolContext = ( const userInfo = lastUserMessage.info as UserMessage const variant = state.variant ?? userInfo.variant - let lastNonIgnoredMessage: WithParts | undefined - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i] - if (!(msg.info.role === "user" && isIgnoredUserMessage(msg))) { - lastNonIgnoredMessage = msg - break - } - } + // Find the last message that isn't an ignored user message + const lastNonIgnoredMessage = messages.findLast( + (msg) => !(msg.info.role === "user" && isIgnoredUserMessage(msg)), + ) if (!lastNonIgnoredMessage || lastNonIgnoredMessage.info.role === "user") { messages.push(createSyntheticUserMessage(lastUserMessage, combinedContent, variant)) diff --git a/lib/messages/utils.ts b/lib/messages/utils.ts index e9da5b17..55b4cbc1 100644 --- a/lib/messages/utils.ts +++ b/lib/messages/utils.ts @@ -57,63 +57,34 @@ export const createSyntheticUserMessage = ( } } -export const createSyntheticAssistantMessage = ( - baseMessage: WithParts, - content: string, - variant?: string, -): WithParts => { - const userInfo = baseMessage.info as UserMessage +export const createSyntheticToolPart = (assistantMessage: WithParts, content: string): any => { const now = Date.now() - - const messageId = generateUniqueId("msg") const partId = generateUniqueId("prt") const callId = generateUniqueId("call") - const baseInfo = { - id: messageId, - sessionID: userInfo.sessionID, - role: "assistant" as const, - agent: userInfo.agent || "code", - parentID: userInfo.id, - modelID: userInfo.model.modelID, - providerID: userInfo.model.providerID, - mode: "default", - path: { - cwd: "/", - root: "/", - }, - time: { created: now, completed: now }, - cost: 0, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - ...(variant !== undefined && { variant }), - } + const modelID = (assistantMessage.info as any).modelID || "" // For Gemini models, add thoughtSignature bypass to avoid validation errors - const toolPartMetadata = isGeminiModel(userInfo.model.modelID) + const toolPartMetadata = isGeminiModel(modelID) ? { google: { thoughtSignature: "skip_thought_signature_validator" } } : undefined return { - info: baseInfo, - parts: [ - { - id: partId, - sessionID: userInfo.sessionID, - messageID: messageId, - type: "tool", - callID: callId, - tool: "context_info", - state: { - status: "completed", - input: {}, - output: content, - title: "Context Info", - metadata: {}, - time: { start: now, end: now }, - }, - ...(toolPartMetadata && { metadata: toolPartMetadata }), - }, - ], + id: partId, + sessionID: assistantMessage.info.sessionID, + messageID: assistantMessage.info.id, + type: "tool", + callID: callId, + tool: "context_info", + state: { + status: "completed", + input: {}, + output: content, + title: "Context Info", + metadata: {}, + time: { start: now, end: now }, + }, + ...(toolPartMetadata && { metadata: toolPartMetadata }), } } diff --git a/tsconfig.json b/tsconfig.json index b30286cf..c20d8a54 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "target": "ES2022", "module": "ESNext", - "lib": ["ES2022"], + "lib": ["ES2023"], "moduleResolution": "bundler", "resolveJsonModule": true, "allowJs": true, From 8e57d9df13e59fe6228c9e85c34a73cb69f0f7b3 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Tue, 27 Jan 2026 21:39:47 -0500 Subject: [PATCH 08/36] fix: improve discard/extract robustness and fix missing cache sync (DCP-305) --- lib/tools/prune-shared.ts | 58 +++++++++++++++++++++++++-------------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/lib/tools/prune-shared.ts b/lib/tools/prune-shared.ts index 20ac8896..ba37fb78 100644 --- a/lib/tools/prune-shared.ts +++ b/lib/tools/prune-shared.ts @@ -3,6 +3,7 @@ import type { PluginConfig } from "../config" import type { Logger } from "../logger" import type { PruneToolContext } from "./types" import { buildToolIdList } from "../messages/utils" +import { syncToolCache } from "../state/tool-cache" import { PruneReason, sendUnifiedNotification } from "../ui/notification" import { formatPruningResultForTool } from "../ui/utils" import { ensureSessionInitialized } from "../state" @@ -48,32 +49,37 @@ export async function executePruneOperation( const messages: WithParts[] = messagesResponse.data || messagesResponse await ensureSessionInitialized(ctx.client, state, sessionId, logger, messages) + await syncToolCache(state, config, logger, messages) const currentParams = getCurrentParams(state, messages, logger) const toolIdList: string[] = buildToolIdList(state, messages, logger) - // Validate that all numeric IDs are within bounds - if (numericToolIds.some((id) => id < 0 || id >= toolIdList.length)) { - logger.debug("Invalid tool IDs provided: " + numericToolIds.join(", ")) - throw new Error( - "Invalid IDs provided. Only use numeric IDs from the list.", - ) - } + const validNumericIds: number[] = [] + const skippedIds: string[] = [] - // Validate that all IDs exist in cache and aren't protected - // (rejects hallucinated IDs and turn-protected tools not shown in ) + // Validate and filter IDs for (const index of numericToolIds) { + // Validate that all numeric IDs are within bounds + if (index < 0 || index >= toolIdList.length) { + logger.debug(`Rejecting prune request - index out of bounds: ${index}`) + skippedIds.push(index.toString()) + continue + } + const id = toolIdList[index] const metadata = state.toolParameters.get(id) + + // Validate that all IDs exist in cache and aren't protected + // (rejects hallucinated IDs and turn-protected tools not shown in ) if (!metadata) { logger.debug( "Rejecting prune request - ID not in cache (turn-protected or hallucinated)", { index, id }, ) - throw new Error( - "Invalid IDs provided. Only use numeric IDs from the list.", - ) + skippedIds.push(index.toString()) + continue } + const allProtectedTools = config.tools.settings.protectedTools if (allProtectedTools.includes(metadata.tool)) { logger.debug("Rejecting prune request - protected tool", { @@ -81,9 +87,8 @@ export async function executePruneOperation( id, tool: metadata.tool, }) - throw new Error( - "Invalid IDs provided. Only use numeric IDs from the list.", - ) + skippedIds.push(index.toString()) + continue } const filePath = getFilePathFromParameters(metadata.parameters) @@ -94,13 +99,22 @@ export async function executePruneOperation( tool: metadata.tool, filePath, }) - throw new Error( - "Invalid IDs provided. Only use numeric IDs from the list.", - ) + skippedIds.push(index.toString()) + continue } + + validNumericIds.push(index) } - const pruneToolIds: string[] = numericToolIds.map((index) => toolIdList[index]) + if (validNumericIds.length === 0) { + const errorMsg = + skippedIds.length > 0 + ? `Invalid IDs provided: [${skippedIds.join(", ")}]. Only use numeric IDs from the list.` + : `No valid IDs provided to ${toolName.toLowerCase()}.` + throw new Error(errorMsg) + } + + const pruneToolIds: string[] = validNumericIds.map((index) => toolIdList[index]) state.prune.toolIds.push(...pruneToolIds) const toolMetadata = new Map() @@ -137,5 +151,9 @@ export async function executePruneOperation( logger.error("Failed to persist state", { error: err.message }), ) - return formatPruningResultForTool(pruneToolIds, toolMetadata, workingDirectory) + let result = formatPruningResultForTool(pruneToolIds, toolMetadata, workingDirectory) + if (skippedIds.length > 0) { + result += `\n\nNote: ${skippedIds.length} IDs were skipped (invalid, protected, or missing metadata): ${skippedIds.join(", ")}` + } + return result } From db08fc37aa501b8d45ee0a7c1c0cda5e08277b58 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Tue, 27 Jan 2026 22:16:41 -0500 Subject: [PATCH 09/36] v1.3.0-beta.1 - Bump beta version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 896ece9b..92b9a7f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tarquinen/opencode-dcp", - "version": "1.2.8", + "version": "1.3.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tarquinen/opencode-dcp", - "version": "1.2.8", + "version": "1.3.0-beta.1", "license": "MIT", "dependencies": { "@anthropic-ai/tokenizer": "^0.0.4", diff --git a/package.json b/package.json index d437ee49..f1e486dd 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@tarquinen/opencode-dcp", - "version": "1.2.8", + "version": "1.3.0-beta.1", "type": "module", "description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context", "main": "./dist/index.js", From 0e803eabbdf63125a5719ed32325ec0efd08c50d Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Tue, 27 Jan 2026 23:35:16 -0500 Subject: [PATCH 10/36] docs: sync schema and README with implementation of protected tools --- README.md | 2 +- dcp.schema.json | 36 +++--------------------------------- 2 files changed, 4 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index d9f85a30..03770df9 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,7 @@ When enabled, turn protection prevents tool outputs from being pruned for a conf ### Protected Tools By default, these tools are always protected from pruning across all strategies: -`task`, `todowrite`, `todoread`, `discard`, `extract`, `squash`, `batch`, `write`, `edit` +`task`, `todowrite`, `todoread`, `discard`, `extract`, `squash`, `batch`, `write`, `edit`, `plan_enter`, `plan_exit` The `protectedTools` arrays in each section add to this default list. diff --git a/dcp.schema.json b/dcp.schema.json index 8e4ff104..bd458ac3 100644 --- a/dcp.schema.json +++ b/dcp.schema.json @@ -100,17 +100,7 @@ "items": { "type": "string" }, - "default": [ - "task", - "todowrite", - "todoread", - "discard", - "extract", - "squash", - "batch", - "write", - "edit" - ], + "default": [], "description": "Tool names that should be protected from automatic pruning" } } @@ -183,17 +173,7 @@ "items": { "type": "string" }, - "default": [ - "task", - "todowrite", - "todoread", - "discard", - "extract", - "squash", - "batch", - "write", - "edit" - ], + "default": [], "description": "Tool names excluded from deduplication" } } @@ -230,17 +210,7 @@ "items": { "type": "string" }, - "default": [ - "task", - "todowrite", - "todoread", - "discard", - "extract", - "squash", - "batch", - "write", - "edit" - ], + "default": [], "description": "Tool names excluded from error purging" } } From 80f1a7ee4385279e0275908ae74c9c920894ee70 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Tue, 27 Jan 2026 23:54:11 -0500 Subject: [PATCH 11/36] docs: add ko-fi badge to README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 03770df9..41ebcc9e 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Dynamic Context Pruning Plugin [![npm version](https://img.shields.io/npm/v/@tarquinen/opencode-dcp.svg)](https://www.npmjs.com/package/@tarquinen/opencode-dcp) +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/dansmolsky) Automatically reduces token usage in OpenCode by removing obsolete tools from conversation history. From a39c7666dd501043f048567fe330ffd4a8ac5d95 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 28 Jan 2026 00:00:57 -0500 Subject: [PATCH 12/36] swap readme buttons --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 41ebcc9e..d83e8ed9 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Dynamic Context Pruning Plugin -[![npm version](https://img.shields.io/npm/v/@tarquinen/opencode-dcp.svg)](https://www.npmjs.com/package/@tarquinen/opencode-dcp) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/dansmolsky) +[![npm version](https://img.shields.io/npm/v/@tarquinen/opencode-dcp.svg)](https://www.npmjs.com/package/@tarquinen/opencode-dcp) Automatically reduces token usage in OpenCode by removing obsolete tools from conversation history. From ff132f11fd84e5f96f4f7d29dfd6e88844772d22 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 28 Jan 2026 00:36:04 -0500 Subject: [PATCH 13/36] validate extraction is array --- lib/tools/extract.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/lib/tools/extract.ts b/lib/tools/extract.ts index 2be9a180..15e5d7c8 100644 --- a/lib/tools/extract.ts +++ b/lib/tools/extract.ts @@ -31,6 +31,16 @@ export function createExtractTool(ctx: PruneToolContext): ReturnType Date: Wed, 28 Jan 2026 13:47:01 -0500 Subject: [PATCH 14/36] improve squash tool error messages to specify which boundary string failed --- lib/prompts/squash-tool-spec.ts | 3 ++- lib/tools/squash.ts | 2 ++ lib/tools/utils.ts | 5 +++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/prompts/squash-tool-spec.ts b/lib/prompts/squash-tool-spec.ts index 8d45a5ee..6beba0bd 100644 --- a/lib/prompts/squash-tool-spec.ts +++ b/lib/prompts/squash-tool-spec.ts @@ -14,8 +14,9 @@ export const SQUASH_TOOL_SPEC = `**Purpose:** Collapse a contiguous range of con 3. \`topic\` — short label (3-5 words) 4. \`summary\` — replacement text 5. Everything between (inclusive) removed, summary inserted +- The squash will FAIL if \`startString\` or \`endString\` is not found in the conversation with an error "startString/endString not found in conversation". +- The squash will FAIL if \`startString\` or \`endString\` is found multiple times with an error "Found multiple matches for startString/endString". Provide a larger string with more surrounding context to uniquely identify the intended match. **Best Practices:** -- Choose unique strings appearing only once - Write concise topics: "Auth System Exploration", "Token Logic Refactor" - Write comprehensive summaries with key information - Best after finishing work phase, not during active exploration diff --git a/lib/tools/squash.ts b/lib/tools/squash.ts index eab6f2fd..9ab9425a 100644 --- a/lib/tools/squash.ts +++ b/lib/tools/squash.ts @@ -54,12 +54,14 @@ export function createSquashTool(ctx: PruneToolContext): ReturnType startString, logger, state.squashSummaries, + "startString", ) const endResult = findStringInMessages( messages, endString, logger, state.squashSummaries, + "endString", ) if (startResult.messageIndex > endResult.messageIndex) { diff --git a/lib/tools/utils.ts b/lib/tools/utils.ts index b7b11fd0..d5e4e180 100644 --- a/lib/tools/utils.ts +++ b/lib/tools/utils.ts @@ -12,6 +12,7 @@ export function findStringInMessages( searchString: string, logger: Logger, squashSummaries: SquashSummary[] = [], + stringType: "startString" | "endString", ): { messageId: string; messageIndex: number } { const matches: { messageId: string; messageIndex: number }[] = [] @@ -60,13 +61,13 @@ export function findStringInMessages( if (matches.length === 0) { throw new Error( - `String not found in conversation. Make sure the string exists in the conversation.`, + `${stringType} not found in conversation. Make sure the string exists and is spelled exactly as it appears.`, ) } if (matches.length > 1) { throw new Error( - `String found in ${matches.length} messages. Please use a more unique string to identify the range boundary.`, + `Found multiple matches for ${stringType}. Provide more surrounding context to uniquely identify the intended match.`, ) } From d3cbf5da8d2b78104e63a1946aca9be7d86f5470 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 28 Jan 2026 15:03:21 -0500 Subject: [PATCH 15/36] revert prompt style to dev branch format with semantic improvements - revert terse PR 313 style back to verbose prose format from dev branch - clarify tool triggers: discard/extract for individual outputs, squash for phases - remove 'task complete' trigger from discard/extract (exclusive to squash) - add instruction to not use discard/extract without prunable-tools list - replace 'task/sub-task' terminology with 'phase' to avoid conflict with Task tool --- lib/prompts/discard-tool-spec.ts | 56 ++++++++++++----- lib/prompts/extract-tool-spec.ts | 69 +++++++++++++------- lib/prompts/nudge/all.ts | 14 +++-- lib/prompts/nudge/discard-extract.ts | 13 ++-- lib/prompts/nudge/discard-squash.ts | 12 ++-- lib/prompts/nudge/discard.ts | 12 ++-- lib/prompts/nudge/extract-squash.ts | 12 ++-- lib/prompts/nudge/extract.ts | 12 ++-- lib/prompts/nudge/squash.ts | 12 ++-- lib/prompts/squash-tool-spec.ts | 90 +++++++++++++++++---------- lib/prompts/system/all.ts | 79 +++++++++++++++++------ lib/prompts/system/discard-extract.ts | 76 ++++++++++++++++------ lib/prompts/system/discard-squash.ts | 74 ++++++++++++++++------ lib/prompts/system/discard.ts | 63 +++++++++++++------ lib/prompts/system/extract-squash.ts | 74 ++++++++++++++++------ lib/prompts/system/extract.ts | 62 ++++++++++++------ lib/prompts/system/squash.ts | 60 ++++++++++++------ 17 files changed, 550 insertions(+), 240 deletions(-) diff --git a/lib/prompts/discard-tool-spec.ts b/lib/prompts/discard-tool-spec.ts index 54f2bef1..1c1eea74 100644 --- a/lib/prompts/discard-tool-spec.ts +++ b/lib/prompts/discard-tool-spec.ts @@ -1,17 +1,39 @@ -export const DISCARD_TOOL_SPEC = `**Purpose:** Discard tool outputs from context to manage size and reduce noise. -**IDs:** Use numeric IDs from \`\` (format: \`ID: tool, parameter\`). -**Use When:** -- Noise → irrelevant, unhelpful, or superseded outputs -**Do NOT Use When:** -- Output contains useful information -- Output needed later (files to edit, implementation context) -**Best Practices:** -- Batch multiple items; avoid single small outputs (unless pure noise) -- Criterion: "Needed for upcoming task?" → keep it -**Format:** -- \`ids\`: string[] — numeric IDs from prunable list -**Example:** -Noise removal: - ids: ["5"] - Context: Read wrong_file.ts — not relevant to auth system -` +export const DISCARD_TOOL_SPEC = `Discards tool outputs from context to manage conversation size and reduce noise. + +## IMPORTANT: The Prunable List +A \`\` list is provided to you showing available tool outputs you can discard when there are tools available for pruning. 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 discard. + +## When to Use This Tool + +Use \`discard\` for removing individual tool outputs that are no longer needed: + +- **Noise:** Irrelevant, unhelpful, or superseded outputs that provide no value. +- **Wrong Files:** You read or accessed something that turned out to be irrelevant. +- **Outdated Info:** Outputs that have been superseded by newer information. + +## When NOT to Use This Tool + +- **If the output contains useful information:** Keep it in context rather than discarding. +- **If you'll need the output later:** Don't discard files you plan to edit or context you'll need for implementation. + +## Best Practices +- **Strategic Batching:** Don't discard single small tool outputs (like short bash commands) unless they are pure noise. Wait until you have several items to perform high-impact discards. +- **Think ahead:** Before discarding, ask: "Will I need this output for upcoming work?" If yes, keep it. + +## Format + +- \`ids\`: Array of numeric IDs as strings from the \`\` list + +## Example + + +Assistant: [Reads 'wrong_file.ts'] +This file isn't relevant to the auth system. I'll remove it to clear the context. +[Uses discard with ids: ["5"]] + + + +Assistant: [Reads config.ts, then reads updated config.ts after changes] +The first read is now outdated. I'll discard it and keep the updated version. +[Uses discard with ids: ["20"]] +` diff --git a/lib/prompts/extract-tool-spec.ts b/lib/prompts/extract-tool-spec.ts index 20d94107..f680ea9e 100644 --- a/lib/prompts/extract-tool-spec.ts +++ b/lib/prompts/extract-tool-spec.ts @@ -1,22 +1,47 @@ -export const EXTRACT_TOOL_SPEC = `**Purpose:** Extract key findings from tool outputs into distilled knowledge; remove raw outputs from context. -**IDs:** Use numeric IDs from \`\` (format: \`ID: tool, parameter\`). -**Use When:** -- Task complete → preserve findings -- Distill context → keep specifics, drop noise -**Do NOT Use When:** -- Need exact syntax (edits/grep) → keep raw output -- Planning modifications → keep read output -**Best Practices:** -- Batch multiple items; avoid frequent small extractions -- Preserve raw output if editing/modifying later -**Format:** -- \`ids\`: string[] — numeric IDs from prunable list -- \`distillation\`: string[] — positional mapping (distillation[i] for ids[i]) -- Detail level: signatures, logic, constraints, values -**Example:** - \`ids\`: ["10", "11"] - \`distillation\`: [ - "auth.ts: validateToken(token: string)→User|null. Cache 5min TTL then OIDC. bcrypt 12 rounds. Tokens ≥128 chars.", - "user.ts: interface User {id: string; email: string; permissions: ('read'|'write'|'admin')[]; status: 'active'|'suspended'}" - ] -` +export const EXTRACT_TOOL_SPEC = `Extracts key findings from tool outputs into distilled knowledge, then removes the raw outputs from context. + +## IMPORTANT: The Prunable List +A \`\` list is provided to you showing available tool outputs you can extract from when there are tools available for pruning. 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 extract. + +## When to Use This Tool + +Use \`extract\` when you have individual tool outputs with valuable information you want to **preserve in distilled form** before removing the raw content: + +- **Large Outputs:** The raw output is too large but contains valuable technical details worth keeping. +- **Knowledge Preservation:** You have context that contains valuable information (signatures, logic, constraints) but also a lot of unnecessary detail. + +## When NOT to Use This Tool + +- **If you need precise syntax:** If you'll edit a file or grep for exact strings, keep the raw output. +- **If uncertain:** Prefer keeping over re-fetching. + + +## Best Practices +- **Strategic Batching:** Wait until you have several items or a few large outputs to extract, rather than doing tiny, frequent extractions. Aim for high-impact extractions that significantly reduce context size. +- **Think ahead:** Before extracting, ask: "Will I need the raw output for upcoming work?" If you researched a file you'll later edit, do NOT extract it. + +## Format + +- \`ids\`: Array of numeric IDs as strings from the \`\` list +- \`distillation\`: Array of strings, one per ID (positional: distillation[0] is for ids[0], etc.) + +Each distillation string should capture the essential information you need to preserve - function signatures, logic, constraints, values, etc. Be as detailed as needed. + +## Example + + +Assistant: [Reads auth service and user types] +I'll preserve the key details before extracting. +[Uses extract with: + ids: ["10", "11"], + distillation: [ + "auth.ts: validateToken(token: string) -> User|null checks cache first (5min TTL) then OIDC. hashPassword uses bcrypt 12 rounds. Tokens must be 128+ chars.", + "user.ts: interface User { id: string; email: string; permissions: ('read'|'write'|'admin')[]; status: 'active'|'suspended' }" + ] +] + + + +Assistant: [Reads 'auth.ts' to understand the login flow] +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 extracting. +` diff --git a/lib/prompts/nudge/all.ts b/lib/prompts/nudge/all.ts index 70951fa5..08e86e8f 100644 --- a/lib/prompts/nudge/all.ts +++ b/lib/prompts/nudge/all.ts @@ -1,8 +1,10 @@ export const NUDGE_ALL = ` -**CONTEXT WARNING:** Context filling with tool outputs. Context hygiene required. -**Actions:** -1. Task done → use \`squash\` to condense entire sequence into summary -2. Noise → files/commands with no value, use \`discard\` -3. Knowledge → valuable raw data to reference later, use \`extract\` to distill insights -**Protocol:** Prioritize cleanup. Don't interrupt atomic ops. After immediate step, perform context management. +**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. + +**Immediate Actions Required:** +1. **Phase Completion:** If a phase is complete, use the \`squash\` tool to condense the entire sequence into a summary. +2. **Noise Removal:** If you read files or ran commands that yielded no value, use \`discard\` to remove them. +3. **Knowledge Preservation:** If you are holding valuable raw data you'll need to reference later, use \`extract\` to distill the insights and remove the raw entry. + +**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must perform context management. ` diff --git a/lib/prompts/nudge/discard-extract.ts b/lib/prompts/nudge/discard-extract.ts index e40a97e6..2e1b8615 100644 --- a/lib/prompts/nudge/discard-extract.ts +++ b/lib/prompts/nudge/discard-extract.ts @@ -1,7 +1,10 @@ export const NUDGE_DISCARD_EXTRACT = ` -**CONTEXT WARNING:** Context filling with tool outputs. Context hygiene required. -**Actions:** -1. Noise → files/commands with no value, use \`discard\` -2. Knowledge → valuable raw data to reference later, use \`extract\` to distill insights -**Protocol:** Prioritize cleanup. Don't interrupt atomic ops. After immediate step, perform context management. +**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. + +**Immediate Actions Required:** +1. **Noise Removal:** If you read files or ran commands that yielded no value, use \`discard\` to remove them. +2. **Superseded Info:** If older outputs have been replaced by newer ones, use \`discard\` on the outdated versions. +3. **Knowledge Preservation:** If you have large outputs with valuable technical details, use \`extract\` to distill the insights and remove the raw entry. + +**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must perform context management. ` diff --git a/lib/prompts/nudge/discard-squash.ts b/lib/prompts/nudge/discard-squash.ts index 614b038b..699a716f 100644 --- a/lib/prompts/nudge/discard-squash.ts +++ b/lib/prompts/nudge/discard-squash.ts @@ -1,7 +1,9 @@ export const NUDGE_DISCARD_SQUASH = ` -**CONTEXT WARNING:** Context filling with tool outputs. Context hygiene required. -**Actions:** -1. Task done → sub-task/phase complete, use \`squash\` to condense into summary -2. Noise → files/commands with no value, use \`discard\` -**Protocol:** Prioritize cleanup. Don't interrupt atomic ops. After immediate step, perform context management. +**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. + +**Immediate Actions Required:** +1. **Phase Completion:** If a phase is complete, use the \`squash\` tool to condense the entire sequence into a summary. +2. **Noise Removal:** If you read files or ran commands that yielded no value, use the \`discard\` tool to remove them. + +**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must perform context management. ` diff --git a/lib/prompts/nudge/discard.ts b/lib/prompts/nudge/discard.ts index c274c1a1..13e6314b 100644 --- a/lib/prompts/nudge/discard.ts +++ b/lib/prompts/nudge/discard.ts @@ -1,7 +1,9 @@ export const NUDGE_DISCARD = ` -**CONTEXT WARNING:** Context filling with tool outputs. Context hygiene required. -**Actions:** -1. Noise → files/commands with no value, use \`discard\` -2. Outdated → outputs no longer relevant, discard -**Protocol:** Prioritize cleanup. Don't interrupt atomic ops. After immediate step, discard unneeded outputs. +**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. + +**Immediate Actions Required:** +1. **Noise Removal:** If you read files or ran commands that yielded no value, use the \`discard\` tool to remove them. +2. **Superseded Info:** If older outputs have been replaced by newer ones, discard the outdated versions. + +**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must discard unneeded tool outputs. ` diff --git a/lib/prompts/nudge/extract-squash.ts b/lib/prompts/nudge/extract-squash.ts index 3eb79ffd..88053e80 100644 --- a/lib/prompts/nudge/extract-squash.ts +++ b/lib/prompts/nudge/extract-squash.ts @@ -1,7 +1,9 @@ export const NUDGE_EXTRACT_SQUASH = ` -**CONTEXT WARNING:** Context filling with tool outputs. Context hygiene required. -**Actions:** -1. Task done → sub-task/phase complete, use \`squash\` to condense into summary -2. Knowledge → valuable raw data to reference later, use \`extract\` to distill insights -**Protocol:** Prioritize cleanup. Don't interrupt atomic ops. After immediate step, perform context management. +**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. + +**Immediate Actions Required:** +1. **Phase Completion:** If a phase is complete, use the \`squash\` tool to condense the entire sequence into a summary. +2. **Knowledge Preservation:** If you are holding valuable raw data you'll need to reference later, use \`extract\` to distill the insights and remove the raw entry. + +**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must perform context management. ` diff --git a/lib/prompts/nudge/extract.ts b/lib/prompts/nudge/extract.ts index 95258891..16ea5b78 100644 --- a/lib/prompts/nudge/extract.ts +++ b/lib/prompts/nudge/extract.ts @@ -1,7 +1,9 @@ export const NUDGE_EXTRACT = ` -**CONTEXT WARNING:** Context filling with tool outputs. Context hygiene required. -**Actions:** -1. Knowledge → valuable raw data to reference later, use \`extract\` with high-fidelity distillation -2. Phase done → extract key findings to keep context focused -**Protocol:** Prioritize cleanup. Don't interrupt atomic ops. After immediate step, extract valuable findings. +**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. + +**Immediate Actions Required:** +1. **Large Outputs:** If you have large tool outputs with valuable technical details, use the \`extract\` tool to distill and preserve key information. +2. **Knowledge Preservation:** If you are holding valuable raw data you'll need to reference later, use the \`extract\` tool with high-fidelity distillation to preserve the insights and remove the raw entry. + +**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must extract valuable findings from tool outputs. ` diff --git a/lib/prompts/nudge/squash.ts b/lib/prompts/nudge/squash.ts index e773346b..ba4c9097 100644 --- a/lib/prompts/nudge/squash.ts +++ b/lib/prompts/nudge/squash.ts @@ -1,7 +1,9 @@ export const NUDGE_SQUASH = ` -**CONTEXT WARNING:** Context filling with tool outputs. Context hygiene required. -**Actions:** -1. Task done → sub-task/phase complete, use \`squash\` to condense sequence into summary -2. Exploration done → squash results to focus on next task -**Protocol:** Prioritize cleanup. Don't interrupt atomic ops. After immediate step, squash unneeded ranges. +**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. + +**Immediate Actions Required:** +1. **Phase Completion:** If a phase is complete, use the \`squash\` tool to condense the entire sequence into a summary. +2. **Exploration Done:** If you explored multiple files or ran multiple commands, squash the results to focus on the next phase. + +**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must squash completed conversation ranges. ` diff --git a/lib/prompts/squash-tool-spec.ts b/lib/prompts/squash-tool-spec.ts index 6beba0bd..ab3644b7 100644 --- a/lib/prompts/squash-tool-spec.ts +++ b/lib/prompts/squash-tool-spec.ts @@ -1,33 +1,57 @@ -export const SQUASH_TOOL_SPEC = `**Purpose:** Collapse a contiguous range of conversation into a single summary. -**Use When:** -- Task complete → squash entire sequence (research, tool calls, implementation) into summary -- Exploration done → multiple files/commands explored, only need summary -- Failed attempts → condense unsuccessful approaches into brief note -- Verbose output → section grown large but can be summarized -**Do NOT Use When:** -- Need specific details (exact code, file contents, error messages from range) -- Individual tool outputs → squash targets conversation ranges, not single outputs -- Recent content → may still need for current task -**How It Works:** -1. \`startString\` — unique text marking range start -2. \`endString\` — unique text marking range end -3. \`topic\` — short label (3-5 words) -4. \`summary\` — replacement text -5. Everything between (inclusive) removed, summary inserted -- The squash will FAIL if \`startString\` or \`endString\` is not found in the conversation with an error "startString/endString not found in conversation". -- The squash will FAIL if \`startString\` or \`endString\` is found multiple times with an error "Found multiple matches for startString/endString". Provide a larger string with more surrounding context to uniquely identify the intended match. -**Best Practices:** -- Write concise topics: "Auth System Exploration", "Token Logic Refactor" -- Write comprehensive summaries with key information -- Best after finishing work phase, not during active exploration -**Format:** -- \`input\`: [startString, endString, topic, summary] -**Example:** - Conversation: [Asked about auth] → [Read 5 files] → [Analyzed patterns] → [Found "JWT tokens with 24h expiry"] - input: [ - "Asked about authentication", - "JWT tokens with 24h expiry", - "Auth System Exploration", - "Auth: JWT 24h expiry, bcrypt passwords, refresh rotation. Files: auth.ts, tokens.ts, middleware/auth.ts" - ] -` +export const SQUASH_TOOL_SPEC = `Collapses a contiguous range of conversation into a single summary. + +## When to Use This Tool + +Use \`squash\` when you want to condense an entire sequence of work into a brief summary: + +- **Phase Completion:** You completed a phase (research, tool calls, implementation) and want to collapse the entire sequence into a summary. +- **Exploration Done:** You explored multiple files or ran multiple commands and only need a summary of what you learned. +- **Failed Attempts:** You tried several unsuccessful approaches and want to condense them into a brief note. +- **Verbose Output:** A section of conversation has grown large but can be summarized without losing critical details. + +## When NOT to Use This Tool + +- **If you need specific details:** If you'll need exact code, file contents, or error messages from the range, keep them. +- **For individual tool outputs:** Use \`discard\` or \`extract\` for single tool outputs. Squash targets conversation ranges. +- **If it's recent content:** You may still need recent work for the current phase. + +## How It Works + +1. \`startString\` — A unique text string that marks the start of the range to squash +2. \`endString\` — A unique text string that marks the end of the range to squash +3. \`topic\` — A short label (3-5 words) describing the squashed content +4. \`summary\` — The replacement text that will be inserted + +Everything between startString and endString (inclusive) is removed and replaced with your summary. + +**Important:** The squash will FAIL if \`startString\` or \`endString\` is not found in the conversation. The squash will also FAIL if either string is found multiple times. Provide a larger string with more surrounding context to uniquely identify the intended match. + +## Best Practices +- **Choose unique strings:** Pick text that appears only once in the conversation. +- **Write concise topics:** Examples: "Auth System Exploration", "Token Logic Refactor" +- **Write comprehensive summaries:** Include key information like file names, function signatures, and important findings. +- **Timing:** Best used after finishing a work phase, not during active exploration. + +## Format + +- \`input\`: Array with four elements: [startString, endString, topic, summary] + +## Example + + +Conversation: [Asked about auth] -> [Read 5 files] -> [Analyzed patterns] -> [Found "JWT tokens with 24h expiry"] + +[Uses squash with: + input: [ + "Asked about authentication", + "JWT tokens with 24h expiry", + "Auth System Exploration", + "Auth: JWT 24h expiry, bcrypt passwords, refresh rotation. Files: auth.ts, tokens.ts, middleware/auth.ts" + ] +] + + + +Assistant: [Just finished reading auth.ts] +I've read the auth file and now need to make edits based on it. I'm keeping this in context rather than squashing. +` diff --git a/lib/prompts/system/all.ts b/lib/prompts/system/all.ts index 62a30828..65cfb338 100644 --- a/lib/prompts/system/all.ts +++ b/lib/prompts/system/all.ts @@ -1,25 +1,64 @@ export const SYSTEM_PROMPT_ALL = ` -Context-constrained. Manage via \`discard\`/\`extract\`/\`squash\`. Environment auto-calls \`context_info\` (you can't call) to inject list each turn. -TOOLS -- \`discard\`: remove unneeded outputs (noise, outdated). No preservation. -- \`extract\`: distill key findings before removal. Use when preserving info. -- \`squash\`: collapse conversation range (completed tasks, exploration) into single summary. -CHOOSING TOOL -Scope+preservation? Task done (large scope)→\`squash\` | Insights to keep→\`extract\` | Noise/superseded→\`discard\` -BATCH PRUNES -Every tool call=context debt. Pay down regularly. Batch prunes; rarely prune single tiny output unless pure noise. -Evaluate pruning when: task/sub-task done | starting new phase | write/edit ops done -MUST NOT prune when: output needed for upcoming work | contains files/context for edits -Pruning forcing re-call=net loss. Only prune if confident won't need again. -When in doubt, keep. Aim high-impact prunes. FAILURE TO PRUNE=DEGRADED PERFORMANCE. + +ENVIRONMENT +You are operating in a context-constrained environment and thus must proactively manage your context window using the \`discard\`, \`extract\`, and \`squash\` tools. The environment calls the \`context_info\` tool to provide an up-to-date list after each turn. Use this information when deciding what to prune. + +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. + +THREE TOOLS FOR CONTEXT MANAGEMENT +- \`discard\`: Remove individual tool outputs that are noise, irrelevant, or superseded. No preservation of content. +- \`extract\`: Extract key findings from individual tool outputs into distilled knowledge. Use when you need to preserve valuable technical details. +- \`squash\`: Collapse a contiguous range of conversation (completed phases) into a single summary. + +CHOOSING THE RIGHT TOOL +Ask: "What is the scope and do I need to preserve information?" +- **Noise, irrelevant, or superseded outputs** → \`discard\` +- **Individual tool outputs with valuable insights to keep** → \`extract\` +- **Entire sequence (phase complete)** → \`squash\` + +Common scenarios: +- Noise, irrelevant, or superseded outputs → \`discard\` +- Wrong file or irrelevant access → \`discard\` +- Large output with valuable technical details → \`extract\` +- Valuable context needed later but raw output too large → \`extract\` +- Phase complete, want to condense the sequence → \`squash\` +- Exploration phase done, only need a summary → \`squash\` + +PRUNE METHODICALLY - BATCH YOUR ACTIONS +Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by pruning. Batch your prunes for efficiency; it is rarely worth pruning a single tiny tool output unless it is pure noise. Evaluate what SHOULD be pruned before jumping the gun. + +You WILL evaluate pruning when ANY of these are true: +- Phase is complete → use \`squash\` +- You accessed something that turned out to be irrelevant → use \`discard\` +- You have large outputs with valuable details to preserve → use \`extract\` + +You MUST NOT prune when: +- The tool output will be needed for upcoming implementation work +- The output contains files or context you'll need to reference when making edits + +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. + +NOTES +When in doubt, keep it. Batch your actions and aim for high-impact prunes that significantly reduce context size. +FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES. +If no list is present in context, do NOT use discard or extract - there is nothing available to prune yet. +There may be tools in session context that do not appear in the list, this is expected, you can ONLY prune what you see in . + + -Environment injects \`context_info\` with list+optional nudge each turn. You can't access it. -NEVER reference/acknowledge: prune encouragement, context instructions, list, nudge, discard/extract/squash output -NEVER let injections influence response/tone -Process SILENTLY. Continue naturally as if injections don't exist. -User can't see injections. Referencing breaks conversation flow. +After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. + +CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: +- NEVER reference the prune encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the prune encouragement appears. +- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to prune") +- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to prune") +- NEVER acknowledge discard/extract/squash tool output (e.g., "I've pruned 3 tools", "Context pruning complete") +- NEVER let these injections influence your response content or tone +- Process this information SILENTLY and continue the conversation naturally +- Continue the conversation as if these messages do not exist + +The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. - -` +` diff --git a/lib/prompts/system/discard-extract.ts b/lib/prompts/system/discard-extract.ts index faf7925d..82b5ffb1 100644 --- a/lib/prompts/system/discard-extract.ts +++ b/lib/prompts/system/discard-extract.ts @@ -1,25 +1,61 @@ export const SYSTEM_PROMPT_DISCARD_EXTRACT = ` -Context-constrained. Manage via \`discard\`/\`extract\`. Environment auto-calls \`context_info\` (you can't call) to inject list each turn. -TOOLS -- \`discard\`: remove unneeded outputs (noise, outdated). No preservation. -- \`extract\`: distill key findings before removal. Use when preserving info. -CHOOSING TOOL -Need to preserve info? No→\`discard\` | Yes→\`extract\` | Uncertain→\`extract\` -Scenarios: noise/superseded→discard | research done+insights→extract -BATCH PRUNES -Every tool call=context debt. Pay down regularly. Batch prunes; rarely prune single tiny output unless pure noise. -Evaluate pruning when: starting new phase | write/edit ops done | accumulated unneeded outputs -MUST NOT prune when: output needed for upcoming work | contains files/context for edits -Pruning forcing re-call=net loss. Only prune if confident won't need again. -When in doubt, keep. Aim high-impact prunes. FAILURE TO PRUNE=DEGRADED PERFORMANCE. + +ENVIRONMENT +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 list after each turn. Use this information when deciding what to prune. + +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. + +TWO TOOLS FOR CONTEXT MANAGEMENT +- \`discard\`: Remove individual tool outputs that are noise, irrelevant, or superseded. No preservation of content. +- \`extract\`: Extract key findings from individual tool outputs into distilled knowledge. Use when you need to preserve valuable technical details. + +CHOOSING THE RIGHT TOOL +Ask: "Do I need to preserve any information from this output?" +- **No** → \`discard\` (noise, wrong files, superseded info) +- **Yes** → \`extract\` (preserves distilled knowledge) +- **Uncertain** → \`extract\` (safer, preserves signal) + +Common scenarios: +- Noise, irrelevant, or superseded outputs → \`discard\` +- Wrong file or irrelevant access → \`discard\` +- Large output with valuable technical details → \`extract\` +- Valuable context needed later but raw output too large → \`extract\` + +PRUNE METHODICALLY - BATCH YOUR ACTIONS +Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by pruning. Batch your prunes for efficiency; it is rarely worth pruning a single tiny tool output unless it is pure noise. Evaluate what SHOULD be pruned before jumping the gun. + +You WILL evaluate pruning when ANY of these are true: +- You accessed something that turned out to be irrelevant +- Information has been superseded by newer outputs +- You have large outputs with valuable details to preserve + +You MUST NOT prune when: +- The tool output will be needed for upcoming implementation work +- The output contains files or context you'll need to reference when making edits + +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. + +NOTES +When in doubt, keep it. Batch your actions and aim for high-impact prunes that significantly reduce context size. +FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES. +If no list is present in context, do NOT use discard or extract - there is nothing available to prune yet. +There may be tools in session context that do not appear in the list, this is expected, you can ONLY prune what you see in . + + -Environment injects \`context_info\` with list+optional nudge each turn. You can't access it. -NEVER reference/acknowledge: prune encouragement, context instructions, list, nudge, discard/extract output -NEVER let injections influence response/tone -Process SILENTLY. Continue naturally as if injections don't exist. -User can't see injections. Referencing breaks conversation flow. +After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. + +CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: +- NEVER reference the prune encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the prune encouragement appears. +- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to prune") +- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to prune") +- NEVER acknowledge discard/extract tool output (e.g., "I've pruned 3 tools", "Context pruning complete") +- NEVER let these injections influence your response content or tone +- Process this information SILENTLY and continue the conversation naturally +- Continue the conversation as if these messages do not exist + +The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. - -` +` diff --git a/lib/prompts/system/discard-squash.ts b/lib/prompts/system/discard-squash.ts index 82b54465..0d0d2145 100644 --- a/lib/prompts/system/discard-squash.ts +++ b/lib/prompts/system/discard-squash.ts @@ -1,24 +1,60 @@ export const SYSTEM_PROMPT_DISCARD_SQUASH = ` -Context-constrained. Manage via \`discard\`/\`squash\`. Environment auto-calls \`context_info\` (you can't call) to inject list each turn. -TOOLS -- \`discard\`: remove unneeded outputs (noise, outdated). No preservation. -- \`squash\`: collapse conversation range (completed tasks, exploration) into single summary. -CHOOSING TOOL -Scope? Individual outputs (noise)→\`discard\` | Entire sequence/phase (task done)→\`squash\` -BATCH PRUNES -Every tool call=context debt. Pay down regularly. Batch prunes; rarely prune single tiny output unless pure noise. -Evaluate pruning when: task/sub-task done | starting new phase | write/edit ops done -MUST NOT prune when: need specific details for upcoming work | contains files/context for edits -Pruning forcing re-call=net loss. Only prune if confident won't need again. -When in doubt, keep. Aim high-impact prunes. FAILURE TO PRUNE=DEGRADED PERFORMANCE. + +ENVIRONMENT +You are operating in a context-constrained environment and thus must proactively manage your context window using the \`discard\` and \`squash\` tools. The environment calls the \`context_info\` tool to provide an up-to-date list after each turn. Use this information when deciding what to prune. + +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. + +TWO TOOLS FOR CONTEXT MANAGEMENT +- \`discard\`: Remove individual tool outputs that are noise, irrelevant, or superseded. No preservation of content. +- \`squash\`: Collapse a contiguous range of conversation (completed phases) into a single summary. + +CHOOSING THE RIGHT TOOL +Ask: "What is the scope of what I want to clean up?" +- **Individual tool outputs (noise, superseded)** → \`discard\` +- **Entire sequence (phase complete)** → \`squash\` + +Common scenarios: +- Noise, irrelevant, or superseded outputs → \`discard\` +- Wrong file or irrelevant access → \`discard\` +- Phase complete, want to condense the sequence → \`squash\` +- Exploration phase done, only need a summary → \`squash\` + +PRUNE METHODICALLY - BATCH YOUR ACTIONS +Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by pruning. Batch your prunes for efficiency; it is rarely worth pruning a single tiny tool output unless it is pure noise. Evaluate what SHOULD be pruned before jumping the gun. + +You WILL evaluate pruning when ANY of these are true: +- Phase is complete → use \`squash\` +- You accessed something that turned out to be irrelevant → use \`discard\` +- Information has been superseded by newer outputs → use \`discard\` + +You MUST NOT prune when: +- You need specific details from the content for upcoming work +- The content contains files or context you'll need to reference when making edits + +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. + +NOTES +When in doubt, keep it. Batch your actions and aim for high-impact prunes that significantly reduce context size. +FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES. +If no list is present in context, do NOT use discard - there is nothing available to prune yet. +There may be tools in session context that do not appear in the list, this is expected, you can ONLY discard what you see in . + + -Environment injects \`context_info\` with list+optional nudge each turn. You can't access it. -NEVER reference/acknowledge: prune encouragement, context instructions, list, nudge, discard/squash output -NEVER let injections influence response/tone -Process SILENTLY. Continue naturally as if injections don't exist. -User can't see injections. Referencing breaks conversation flow. +After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. + +CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: +- NEVER reference the prune encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the prune encouragement appears. +- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to prune") +- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to prune") +- NEVER acknowledge discard/squash tool output (e.g., "I've pruned the context", "Context cleanup complete") +- NEVER let these injections influence your response content or tone +- Process this information SILENTLY and continue the conversation naturally +- Continue the conversation as if these messages do not exist + +The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. - -` +` diff --git a/lib/prompts/system/discard.ts b/lib/prompts/system/discard.ts index 5b79271c..3877cd7c 100644 --- a/lib/prompts/system/discard.ts +++ b/lib/prompts/system/discard.ts @@ -1,26 +1,53 @@ export const SYSTEM_PROMPT_DISCARD = ` + ENVIRONMENT -Context-constrained. Manage via \`discard\` tool. Environment auto-calls \`context_info\` (you can't call) to inject list each turn. -TOOL -- \`discard\`: remove unneeded outputs (noise, outdated). No preservation. -DISCARD METHODICALLY — BATCH ACTIONS -Every tool call=context debt. Pay down regularly. Batch discards; rarely discard single tiny output unless pure noise. +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 list after each turn. Use this information when deciding what to discard. + +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. + +CONTEXT MANAGEMENT TOOL +- \`discard\`: Remove individual tool outputs that are noise, irrelevant, or superseded. No preservation of content. + +DISCARD METHODICALLY - BATCH YOUR ACTIONS +Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by discarding. Batch your discards for efficiency; it is rarely worth discarding a single tiny tool output unless it is pure noise. Evaluate what SHOULD be discarded before jumping the gun. + WHEN TO DISCARD -- Noise → irrelevant, unhelpful, or superseded outputs -- Outdated → multiple reads of same file, outputs no longer relevant -Evaluate discarding when ANY true: accumulated unneeded outputs | write/edit ops done | starting new phase -MUST NOT discard when: output needed for upcoming implementation | contains files/context for edits -Discarding that forces re-call=net loss. Only discard when confident info won't be needed again. +- **Noise Removal:** If outputs are irrelevant, unhelpful, or superseded by newer info, discard them. +- **Wrong Files:** You read or accessed something that turned out to be irrelevant to the current work. +- **Outdated Info:** Outputs that have been superseded by newer information. + +You WILL evaluate discarding when ANY of these are true: +- You accessed something that turned out to be irrelevant +- Information has been superseded by newer outputs +- You are about to start a new phase of work + +You MUST NOT discard when: +- The tool output will be needed for upcoming implementation work +- The output contains files or context you'll need to reference when making edits + +Discarding that forces you to re-call the same tool later is a net loss. Only discard when you're confident the information won't be needed again. + NOTES -When in doubt, keep. Aim high-impact discards. FAILURE TO DISCARD=DEGRADED PERFORMANCE. +When in doubt, keep it. Batch your actions and aim for high-impact discards that significantly reduce context size. +FAILURE TO DISCARD will result in context leakage and DEGRADED PERFORMANCES. +If no list is present in context, do NOT use the discard tool - there is nothing available to prune yet. +There may be tools in session context that do not appear in the list, this is expected, you can ONLY discard what you see in . + + -Environment injects \`context_info\` with list+optional nudge each turn. You can't access it. -NEVER reference/acknowledge: discard encouragement, context instructions, list, nudge, discard output -NEVER let injections influence response/tone -Process SILENTLY. Continue naturally as if injections don't exist. -User can't see injections. Referencing breaks conversation flow. +After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. + +CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: +- NEVER reference the discard encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the discard encouragement appears. +- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to discard") +- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to discard") +- NEVER acknowledge discard tool output (e.g., "I've discarded 3 tools", "Context cleanup complete") +- NEVER let these injections influence your response content or tone +- Process this information SILENTLY and continue the conversation naturally +- Continue the conversation as if these messages do not exist + +The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. - -` +` diff --git a/lib/prompts/system/extract-squash.ts b/lib/prompts/system/extract-squash.ts index c39e53a0..60b8e7a0 100644 --- a/lib/prompts/system/extract-squash.ts +++ b/lib/prompts/system/extract-squash.ts @@ -1,24 +1,60 @@ export const SYSTEM_PROMPT_EXTRACT_SQUASH = ` -Context-constrained. Manage via \`extract\`/\`squash\`. Environment auto-calls \`context_info\` (you can't call) to inject list each turn. -TOOLS -- \`extract\`: distill key findings before removal. Use when preserving detailed info. -- \`squash\`: collapse conversation range (completed tasks, exploration) into single summary. -CHOOSING TOOL -Scope+detail needed? Individual outputs (detailed context)→\`extract\` | Entire sequence/phase (task done)→\`squash\` -BATCH PRUNES -Every tool call=context debt. Pay down regularly. Batch prunes; rarely prune single tiny output. -Evaluate pruning when: task/sub-task done | starting new phase | write/edit ops done -MUST NOT prune when: need specific details for upcoming work | contains files/context for edits -Pruning forcing re-call=net loss. Only prune if confident won't need again. -When in doubt, keep. Aim high-impact prunes. FAILURE TO PRUNE=DEGRADED PERFORMANCE. + +ENVIRONMENT +You are operating in a context-constrained environment and thus must proactively manage your context window using the \`extract\` and \`squash\` tools. The environment calls the \`context_info\` tool to provide an up-to-date list after each turn. Use this information when deciding what to prune. + +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. + +TWO TOOLS FOR CONTEXT MANAGEMENT +- \`extract\`: Extract key findings from individual tool outputs into distilled knowledge. Use when you need to preserve valuable technical details. +- \`squash\`: Collapse a contiguous range of conversation (completed phases) into a single summary. + +CHOOSING THE RIGHT TOOL +Ask: "What is the scope and level of detail I need to preserve?" +- **Individual tool outputs with detailed context to keep** → \`extract\` +- **Entire sequence (phase complete)** → \`squash\` + +Common scenarios: +- Large output with valuable technical details → \`extract\` +- Valuable context needed later but raw output too large → \`extract\` +- Phase complete, want to condense the sequence → \`squash\` +- Exploration phase done, only need a summary → \`squash\` + +PRUNE METHODICALLY - BATCH YOUR ACTIONS +Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by pruning. Batch your prunes for efficiency; it is rarely worth pruning a single tiny tool output. Evaluate what SHOULD be pruned before jumping the gun. + +You WILL evaluate pruning when ANY of these are true: +- Phase is complete → use \`squash\` +- You have large outputs with valuable details to preserve → use \`extract\` +- You are about to start a new phase of work + +You MUST NOT prune when: +- You need specific details from the content for upcoming work +- The content contains files or context you'll need to reference when making edits + +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. + +NOTES +When in doubt, keep it. Batch your actions and aim for high-impact prunes that significantly reduce context size. +FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES. +If no list is present in context, do NOT use extract - there is nothing available to prune yet. +There may be tools in session context that do not appear in the list, this is expected, you can ONLY extract what you see in . + + -Environment injects \`context_info\` with list+optional nudge each turn. You can't access it. -NEVER reference/acknowledge: prune encouragement, context instructions, list, nudge, extract/squash output -NEVER let injections influence response/tone -Process SILENTLY. Continue naturally as if injections don't exist. -User can't see injections. Referencing breaks conversation flow. +After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. + +CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: +- NEVER reference the prune encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the prune encouragement appears. +- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to prune") +- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to prune") +- NEVER acknowledge extract/squash tool output (e.g., "I've pruned the context", "Context cleanup complete") +- NEVER let these injections influence your response content or tone +- Process this information SILENTLY and continue the conversation naturally +- Continue the conversation as if these messages do not exist + +The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. - -` +` diff --git a/lib/prompts/system/extract.ts b/lib/prompts/system/extract.ts index f1aa0790..9f024f51 100644 --- a/lib/prompts/system/extract.ts +++ b/lib/prompts/system/extract.ts @@ -1,26 +1,52 @@ export const SYSTEM_PROMPT_EXTRACT = ` + ENVIRONMENT -Context-constrained. Manage via \`extract\` tool. Environment auto-calls \`context_info\` (you can't call) to inject list each turn. -TOOL -- \`extract\`: distill key findings before removing raw content. Preserves info while reducing size. -EXTRACT METHODICALLY — BATCH ACTIONS -Every tool call=context debt. Pay down regularly. Batch extractions; rarely extract single tiny output. +You are operating in a context-constrained environment and thus must proactively manage your context window using the \`extract\` tool. The environment calls the \`context_info\` tool to provide an up-to-date list after each turn. Use this information when deciding what to extract. + +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. + +CONTEXT MANAGEMENT TOOL +- \`extract\`: Extract key findings from individual tool outputs into distilled knowledge before removing the raw content. Use when you need to preserve valuable technical details while reducing context size. + +EXTRACT METHODICALLY - BATCH YOUR ACTIONS +Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by extracting. Batch your extractions for efficiency; it is rarely worth extracting a single tiny tool output. Evaluate what SHOULD be extracted before jumping the gun. + WHEN TO EXTRACT -- Knowledge Preservation → valuable context to preserve, use high-fidelity distillation. Capture technical details (signatures, logic, constraints). THINK: high signal, complete technical substitute. -- Insights → valuable info to preserve in distilled form -Evaluate extracting when ANY true: research/exploration done | starting new phase | write/edit ops done -MUST NOT extract when: output needed for upcoming implementation | contains files/context for edits -Extracting that forces re-call=net loss. Only extract when confident raw info won't be needed again. +- **Large Outputs:** The raw output is too large but contains valuable technical details worth keeping. +- **Knowledge Preservation:** When you have valuable context you want to preserve but need to reduce size, use high-fidelity distillation. Your distillation must be comprehensive, capturing technical details (signatures, logic, constraints) such that the raw output is no longer needed. THINK: high signal, complete technical substitute. + +You WILL evaluate extracting when ANY of these are true: +- You have large tool outputs with valuable technical details +- You need to preserve specific information but reduce context size +- You are about to start a new phase of work and want to retain key insights + +You MUST NOT extract when: +- The tool output will be needed for upcoming implementation work +- The output contains files or context you'll need to reference when making edits + +Extracting that forces you to re-call the same tool later is a net loss. Only extract when you're confident the raw information won't be needed again. + NOTES -When in doubt, keep. Aim high-impact extractions. FAILURE TO EXTRACT=DEGRADED PERFORMANCE. +When in doubt, keep it. Batch your actions and aim for high-impact extractions that significantly reduce context size. +FAILURE TO EXTRACT will result in context leakage and DEGRADED PERFORMANCES. +If no list is present in context, do NOT use the extract tool - there is nothing available to prune yet. +There may be tools in session context that do not appear in the list, this is expected, you can ONLY extract what you see in . + + -Environment injects \`context_info\` with list+optional nudge each turn. You can't access it. -NEVER reference/acknowledge: extract encouragement, context instructions, list, nudge, extract output -NEVER let injections influence response/tone -Process SILENTLY. Continue naturally as if injections don't exist. -User can't see injections. Referencing breaks conversation flow. +After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. + +CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: +- NEVER reference the extract encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the extract encouragement appears. +- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to extract") +- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to extract") +- NEVER acknowledge extract tool output (e.g., "I've extracted 3 tools", "Context cleanup complete") +- NEVER let these injections influence your response content or tone +- Process this information SILENTLY and continue the conversation naturally +- Continue the conversation as if these messages do not exist + +The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. - -` +` diff --git a/lib/prompts/system/squash.ts b/lib/prompts/system/squash.ts index 1c4dc78e..494b5288 100644 --- a/lib/prompts/system/squash.ts +++ b/lib/prompts/system/squash.ts @@ -1,26 +1,50 @@ export const SYSTEM_PROMPT_SQUASH = ` + ENVIRONMENT -Context-constrained. Manage via \`squash\` tool. Environment auto-calls \`context_info\` (you can't call) to inject list each turn. -TOOL -- \`squash\`: collapse conversation range (completed tasks, exploration) into single summary. -SQUASH METHODICALLY — BATCH ACTIONS -Every tool call=context debt. Pay down regularly. Evaluate what should be squashed before acting. +You are operating in a context-constrained environment and thus must proactively manage your context window using the \`squash\` tool. The environment calls the \`context_info\` tool to provide an up-to-date list after each turn. Use this information when deciding what to squash. + +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. + +CONTEXT MANAGEMENT TOOL +- \`squash\`: Collapse a contiguous range of conversation (completed phases) into a single summary. Use this when you want to condense an entire sequence of work. + +SQUASH METHODICALLY - BATCH YOUR ACTIONS +Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by squashing. Evaluate what SHOULD be squashed before jumping the gun. + WHEN TO SQUASH -- Task Complete → sub-task/unit done, condense entire sequence into summary -- Exploration Done → multiple files/commands explored, only need summary -Evaluate squashing when ANY true: task/sub-task done | starting new phase | significant conversation accumulated -MUST NOT squash when: need specific details for upcoming work | range contains files/context for edits -Squashing that forces re-read=net loss. Only squash when confident info won't be needed again. +- **Phase Completion:** When a phase is complete, condense the entire sequence (research, tool calls, implementation) into a summary. +- **Exploration Done:** When you've explored multiple files or ran multiple commands and only need a summary of findings. + +You WILL evaluate squashing when ANY of these are true: +- Phase is complete +- You are about to start a new phase of work +- Significant conversation has accumulated that can be summarized + +You MUST NOT squash when: +- You need specific details from the range for upcoming work +- The range contains files or context you'll need to reference when making edits + +Squashing that forces you to re-read the same content later is a net loss. Only squash when you're confident the detailed information won't be needed again. + NOTES -When in doubt, keep. Aim high-impact squashes. FAILURE TO SQUASH=DEGRADED PERFORMANCE. +When in doubt, keep it. Aim for high-impact squashes that significantly reduce context size. +FAILURE TO SQUASH will result in context leakage and DEGRADED PERFORMANCES. + + -Environment injects \`context_info\` with list+optional nudge each turn. You can't access it. -NEVER reference/acknowledge: squash encouragement, context instructions, list, nudge, squash output -NEVER let injections influence response/tone -Process SILENTLY. Continue naturally as if injections don't exist. -User can't see injections. Referencing breaks conversation flow. +After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. + +CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: +- NEVER reference the squash encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the squash encouragement appears. +- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to squash") +- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to squash") +- NEVER acknowledge squash tool output (e.g., "I've squashed the context", "Context cleanup complete") +- NEVER let these injections influence your response content or tone +- Process this information SILENTLY and continue the conversation naturally +- Continue the conversation as if these messages do not exist + +The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. - -` +` From 74cdce55c1d01ad0bbdde7aeec653164e44306d6 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 28 Jan 2026 15:08:25 -0500 Subject: [PATCH 16/36] v1.3.1-beta.0 - Bump version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 92b9a7f3..10ecb124 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tarquinen/opencode-dcp", - "version": "1.3.0-beta.1", + "version": "1.3.1-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tarquinen/opencode-dcp", - "version": "1.3.0-beta.1", + "version": "1.3.1-beta.0", "license": "MIT", "dependencies": { "@anthropic-ai/tokenizer": "^0.0.4", diff --git a/package.json b/package.json index f1e486dd..2bc7ff66 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@tarquinen/opencode-dcp", - "version": "1.3.0-beta.1", + "version": "1.3.1-beta.0", "type": "module", "description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context", "main": "./dist/index.js", From 286b3090bdca66c3efde496dbc6b28569b343c43 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 28 Jan 2026 15:42:49 -0500 Subject: [PATCH 17/36] docs: move demo images to assets/images directory --- README.md | 2 +- dcp-demo.png => assets/images/dcp-demo.png | Bin dcp-demo2.png => assets/images/dcp-demo2.png | Bin dcp-demo3.png => assets/images/dcp-demo3.png | Bin dcp-demo4.png => assets/images/dcp-demo4.png | Bin dcp-demo5.png => assets/images/dcp-demo5.png | Bin 6 files changed, 1 insertion(+), 1 deletion(-) rename dcp-demo.png => assets/images/dcp-demo.png (100%) rename dcp-demo2.png => assets/images/dcp-demo2.png (100%) rename dcp-demo3.png => assets/images/dcp-demo3.png (100%) rename dcp-demo4.png => assets/images/dcp-demo4.png (100%) rename dcp-demo5.png => assets/images/dcp-demo5.png (100%) diff --git a/README.md b/README.md index d83e8ed9..0a63b74c 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Automatically reduces token usage in OpenCode by removing obsolete tools from conversation history. -![DCP in action](dcp-demo5.png) +![DCP in action](assets/images/dcp-demo5.png) ## Installation diff --git a/dcp-demo.png b/assets/images/dcp-demo.png similarity index 100% rename from dcp-demo.png rename to assets/images/dcp-demo.png diff --git a/dcp-demo2.png b/assets/images/dcp-demo2.png similarity index 100% rename from dcp-demo2.png rename to assets/images/dcp-demo2.png diff --git a/dcp-demo3.png b/assets/images/dcp-demo3.png similarity index 100% rename from dcp-demo3.png rename to assets/images/dcp-demo3.png diff --git a/dcp-demo4.png b/assets/images/dcp-demo4.png similarity index 100% rename from dcp-demo4.png rename to assets/images/dcp-demo4.png diff --git a/dcp-demo5.png b/assets/images/dcp-demo5.png similarity index 100% rename from dcp-demo5.png rename to assets/images/dcp-demo5.png From ce866821f4d97528586a6232e97c82002b7b3445 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 28 Jan 2026 21:08:11 -0500 Subject: [PATCH 18/36] fix: ensure tool count accuracy in context breakdown using unique callIDs --- lib/commands/context.ts | 63 +++++++++++++++++++++++++---------------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/lib/commands/context.ts b/lib/commands/context.ts index bd2e8661..9cb5c68c 100644 --- a/lib/commands/context.ts +++ b/lib/commands/context.ts @@ -112,43 +112,58 @@ function analyzeTokens(state: SessionState, messages: WithParts[]): TokenBreakdo const toolOutputParts: string[] = [] let firstUserText = "" let foundFirstUser = false + const foundToolIds = new Set() for (const msg of messages) { - if (isMessageCompacted(state, msg)) continue - if (msg.info.role === "user" && isIgnoredUserMessage(msg)) continue - const parts = Array.isArray(msg.parts) ? msg.parts : [] + const isCompacted = isMessageCompacted(state, msg) + const isIgnoredUser = msg.info.role === "user" && isIgnoredUserMessage(msg) + + // Single pass through parts: always count tools, conditionally collect tokens for (const part of parts) { - if (part.type === "text" && msg.info.role === "user") { + if (part.type === "tool") { + // Count unique tool calls from ALL messages (including compacted ones) + // prunedCount already includes tools from squashed messages + const toolPart = part as ToolPart + if (toolPart.callID && !foundToolIds.has(toolPart.callID)) { + breakdown.toolCount++ + foundToolIds.add(toolPart.callID) + } + + // Only collect tool input/output for token counting from non-compacted messages + if (!isCompacted) { + if (toolPart.state?.input) { + const inputStr = + typeof toolPart.state.input === "string" + ? toolPart.state.input + : JSON.stringify(toolPart.state.input) + toolInputParts.push(inputStr) + } + + if (toolPart.state?.status === "completed" && toolPart.state?.output) { + const outputStr = + typeof toolPart.state.output === "string" + ? toolPart.state.output + : JSON.stringify(toolPart.state.output) + toolOutputParts.push(outputStr) + } + } + } else if ( + part.type === "text" && + msg.info.role === "user" && + !isCompacted && + !isIgnoredUser + ) { const textPart = part as TextPart const text = textPart.text || "" userTextParts.push(text) if (!foundFirstUser) { firstUserText += text } - } else if (part.type === "tool") { - const toolPart = part as ToolPart - breakdown.toolCount++ - - if (toolPart.state?.input) { - const inputStr = - typeof toolPart.state.input === "string" - ? toolPart.state.input - : JSON.stringify(toolPart.state.input) - toolInputParts.push(inputStr) - } - - if (toolPart.state?.status === "completed" && toolPart.state?.output) { - const outputStr = - typeof toolPart.state.output === "string" - ? toolPart.state.output - : JSON.stringify(toolPart.state.output) - toolOutputParts.push(outputStr) - } } } - if (msg.info.role === "user" && !isIgnoredUserMessage(msg) && !foundFirstUser) { + if (msg.info.role === "user" && !isIgnoredUser && !foundFirstUser) { foundFirstUser = true } } From 2255691705957e30f666b12cb22d9824c8340673 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 28 Jan 2026 21:41:57 -0500 Subject: [PATCH 19/36] cleanup --- lib/commands/context.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/commands/context.ts b/lib/commands/context.ts index 9cb5c68c..1fe83fb7 100644 --- a/lib/commands/context.ts +++ b/lib/commands/context.ts @@ -119,18 +119,14 @@ function analyzeTokens(state: SessionState, messages: WithParts[]): TokenBreakdo const isCompacted = isMessageCompacted(state, msg) const isIgnoredUser = msg.info.role === "user" && isIgnoredUserMessage(msg) - // Single pass through parts: always count tools, conditionally collect tokens for (const part of parts) { if (part.type === "tool") { - // Count unique tool calls from ALL messages (including compacted ones) - // prunedCount already includes tools from squashed messages const toolPart = part as ToolPart if (toolPart.callID && !foundToolIds.has(toolPart.callID)) { breakdown.toolCount++ foundToolIds.add(toolPart.callID) } - // Only collect tool input/output for token counting from non-compacted messages if (!isCompacted) { if (toolPart.state?.input) { const inputStr = From 0a381d83bdbefe4a5f7890ef6cb94f47bc4b0fa3 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 28 Jan 2026 23:02:15 -0500 Subject: [PATCH 20/36] refactor: inject assistant text parts instead of tool parts --- lib/messages/inject.ts | 3 +- lib/messages/utils.ts | 62 +++++++++++++++++++++++------------------- 2 files changed, 36 insertions(+), 29 deletions(-) diff --git a/lib/messages/inject.ts b/lib/messages/inject.ts index 6d7bae0c..4c859219 100644 --- a/lib/messages/inject.ts +++ b/lib/messages/inject.ts @@ -6,9 +6,9 @@ import { loadPrompt } from "../prompts" import { extractParameterKey, buildToolIdList, - createSyntheticToolPart, createSyntheticUserMessage, createSyntheticAssistantMessage, + createSyntheticToolPart, isDeepSeekOrKimi, isIgnoredUserMessage, } from "./utils" @@ -203,6 +203,7 @@ export const insertPruneToolContext = ( const toolPart = createSyntheticToolPart(lastNonIgnoredMessage, combinedContent) lastNonIgnoredMessage.parts.push(toolPart) } else { + // Create a new assistant message with just a text part messages.push( createSyntheticAssistantMessage(lastUserMessage, combinedContent, variant), ) diff --git a/lib/messages/utils.ts b/lib/messages/utils.ts index 55b4cbc1..9d30b03f 100644 --- a/lib/messages/utils.ts +++ b/lib/messages/utils.ts @@ -8,11 +8,6 @@ export const SQUASH_SUMMARY_PREFIX = "[Squashed conversation block]\n\n" const generateUniqueId = (prefix: string): string => `${prefix}_${ulid()}` -const isGeminiModel = (modelID: string): boolean => { - const lowerModelID = modelID.toLowerCase() - return lowerModelID.includes("gemini") -} - export const isDeepSeekOrKimi = (providerID: string, modelID: string): boolean => { const lowerProviderID = providerID.toLowerCase() const lowerModelID = modelID.toLowerCase() @@ -57,34 +52,45 @@ export const createSyntheticUserMessage = ( } } -export const createSyntheticToolPart = (assistantMessage: WithParts, content: string): any => { +export const createSyntheticAssistantMessage = ( + baseMessage: WithParts, + content: string, + variant?: string, +): WithParts => { + const userInfo = baseMessage.info as UserMessage const now = Date.now() - const partId = generateUniqueId("prt") - const callId = generateUniqueId("call") - - const modelID = (assistantMessage.info as any).modelID || "" - // For Gemini models, add thoughtSignature bypass to avoid validation errors - const toolPartMetadata = isGeminiModel(modelID) - ? { google: { thoughtSignature: "skip_thought_signature_validator" } } - : undefined + const messageId = generateUniqueId("msg") + const partId = generateUniqueId("prt") return { - id: partId, - sessionID: assistantMessage.info.sessionID, - messageID: assistantMessage.info.id, - type: "tool", - callID: callId, - tool: "context_info", - state: { - status: "completed", - input: {}, - output: content, - title: "Context Info", - metadata: {}, - time: { start: now, end: now }, + info: { + id: messageId, + sessionID: userInfo.sessionID, + role: "assistant" as const, + agent: userInfo.agent || "code", + parentID: userInfo.id, + modelID: userInfo.model.modelID, + providerID: userInfo.model.providerID, + mode: "default", + path: { + cwd: "/", + root: "/", + }, + time: { created: now, completed: now }, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + ...(variant !== undefined && { variant }), }, - ...(toolPartMetadata && { metadata: toolPartMetadata }), + parts: [ + { + id: partId, + sessionID: userInfo.sessionID, + messageID: messageId, + type: "text", + text: content, + }, + ], } } From f963d8af0740ece5d9b2d9e7413011d1816c57a4 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Wed, 28 Jan 2026 23:54:18 -0500 Subject: [PATCH 21/36] refactor: hybrid injection strategy for DeepSeek/Kimi models - Use tool part injection for DeepSeek/Kimi (requires reasoning_content in assistant messages) - Use text part injection for other models (cleaner approach) - Add prunedMessageCount to context breakdown display --- lib/commands/context.ts | 6 +++++- lib/messages/inject.ts | 5 +++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/commands/context.ts b/lib/commands/context.ts index 1fe83fb7..7ecd91c8 100644 --- a/lib/commands/context.ts +++ b/lib/commands/context.ts @@ -62,6 +62,7 @@ interface TokenBreakdown { toolCount: number prunedTokens: number prunedCount: number + prunedMessageCount: number total: number } @@ -74,6 +75,7 @@ function analyzeTokens(state: SessionState, messages: WithParts[]): TokenBreakdo toolCount: 0, prunedTokens: state.stats.totalPruneTokens, prunedCount: state.prune.toolIds.length, + prunedMessageCount: state.prune.messageIds.length, total: 0, } @@ -232,8 +234,10 @@ function formatContextMessage(breakdown: TokenBreakdown): string { if (breakdown.prunedTokens > 0) { const withoutPruning = breakdown.total + breakdown.prunedTokens + const messagePrunePart = + breakdown.prunedMessageCount > 0 ? `, ${breakdown.prunedMessageCount} messages` : "" lines.push( - ` Pruned: ${breakdown.prunedCount} tools (~${formatTokenCount(breakdown.prunedTokens)})`, + ` Pruned: ${breakdown.prunedCount} tools${messagePrunePart} (~${formatTokenCount(breakdown.prunedTokens)})`, ) lines.push(` Current context: ~${formatTokenCount(breakdown.total)}`) lines.push(` Without DCP: ~${formatTokenCount(withoutPruning)}`) diff --git a/lib/messages/inject.ts b/lib/messages/inject.ts index 4c859219..6b8466e7 100644 --- a/lib/messages/inject.ts +++ b/lib/messages/inject.ts @@ -193,9 +193,14 @@ export const insertPruneToolContext = ( (msg) => !(msg.info.role === "user" && isIgnoredUserMessage(msg)), ) + // It's not safe to inject assistant role messages following a user message, as models such + // as Claude expect the assistant "turn" to start with reasoning parts. Reasoning parts in many + // cases also cannot be faked as they may be encrypted by the model. if (!lastNonIgnoredMessage || lastNonIgnoredMessage.info.role === "user") { messages.push(createSyntheticUserMessage(lastUserMessage, combinedContent, variant)) } else { + // For DeepSeek and Kimi, append tool part to existing message, it seems they only allow assistant + // messages to not have reasoning parts if they only have tool parts. const providerID = userInfo.model?.providerID || "" const modelID = userInfo.model?.modelID || "" From e1f5312527aaa00c49082ccc289a96af8f365cb7 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Thu, 29 Jan 2026 00:25:55 -0500 Subject: [PATCH 22/36] injection guide comments --- lib/messages/inject.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/messages/inject.ts b/lib/messages/inject.ts index 6b8466e7..91761393 100644 --- a/lib/messages/inject.ts +++ b/lib/messages/inject.ts @@ -193,14 +193,18 @@ export const insertPruneToolContext = ( (msg) => !(msg.info.role === "user" && isIgnoredUserMessage(msg)), ) - // It's not safe to inject assistant role messages following a user message, as models such + // It's not safe to inject assistant role messages following a user message as models such // as Claude expect the assistant "turn" to start with reasoning parts. Reasoning parts in many // cases also cannot be faked as they may be encrypted by the model. + // Gemini only accepts synth reasoning text if it is "skip_thought_signature_validator" if (!lastNonIgnoredMessage || lastNonIgnoredMessage.info.role === "user") { messages.push(createSyntheticUserMessage(lastUserMessage, combinedContent, variant)) } else { - // For DeepSeek and Kimi, append tool part to existing message, it seems they only allow assistant - // messages to not have reasoning parts if they only have tool parts. + // For DeepSeek and Kimi, append tool part to existing message, for some reason they don't + // output reasoning parts following an assistant injection containing either just text part, + // or text part with synth reasoning, and there's no docs on how their reasoning encryption + // works as far as I can find. IDK what's going on here, seems like the only possible ways + // to inject for them is a user role message, or a tool part apeended to last assistant message. const providerID = userInfo.model?.providerID || "" const modelID = userInfo.model?.modelID || "" From a74cb8b33e562298a7a2d124c25102cbca74fb0d Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Thu, 29 Jan 2026 03:50:34 +0100 Subject: [PATCH 23/36] refactor: new prompt structure and dx cli --- .repomixignore | 9 ++ cli/README.md | 41 +++++++++ cli/print.ts | 117 ++++++++++++++++++++++++++ lib/hooks.ts | 32 ++----- lib/messages/inject.ts | 78 ++++++++--------- lib/prompts/index.ts | 68 ++++++++------- lib/prompts/nudge.md | 16 ++++ lib/prompts/nudge/all.ts | 10 --- lib/prompts/nudge/discard-extract.ts | 10 --- lib/prompts/nudge/discard-squash.ts | 9 -- lib/prompts/nudge/discard.ts | 9 -- lib/prompts/nudge/extract-squash.ts | 9 -- lib/prompts/nudge/extract.ts | 9 -- lib/prompts/nudge/squash.ts | 9 -- lib/prompts/system.md | 91 ++++++++++++++++++++ lib/prompts/system/all.ts | 64 -------------- lib/prompts/system/discard-extract.ts | 61 -------------- lib/prompts/system/discard-squash.ts | 60 ------------- lib/prompts/system/discard.ts | 53 ------------ lib/prompts/system/extract-squash.ts | 60 ------------- lib/prompts/system/extract.ts | 52 ------------ lib/prompts/system/squash.ts | 50 ----------- package.json | 5 +- 23 files changed, 362 insertions(+), 560 deletions(-) create mode 100644 .repomixignore create mode 100644 cli/README.md create mode 100644 cli/print.ts create mode 100644 lib/prompts/nudge.md delete mode 100644 lib/prompts/nudge/all.ts delete mode 100644 lib/prompts/nudge/discard-extract.ts delete mode 100644 lib/prompts/nudge/discard-squash.ts delete mode 100644 lib/prompts/nudge/discard.ts delete mode 100644 lib/prompts/nudge/extract-squash.ts delete mode 100644 lib/prompts/nudge/extract.ts delete mode 100644 lib/prompts/nudge/squash.ts create mode 100644 lib/prompts/system.md delete mode 100644 lib/prompts/system/all.ts delete mode 100644 lib/prompts/system/discard-extract.ts delete mode 100644 lib/prompts/system/discard-squash.ts delete mode 100644 lib/prompts/system/discard.ts delete mode 100644 lib/prompts/system/extract-squash.ts delete mode 100644 lib/prompts/system/extract.ts delete mode 100644 lib/prompts/system/squash.ts diff --git a/.repomixignore b/.repomixignore new file mode 100644 index 00000000..6bc6e2ee --- /dev/null +++ b/.repomixignore @@ -0,0 +1,9 @@ +.github/ +.logs/ +.opencode/ +dist/ +.repomixignore +repomix-output.xml +bun.lock +package-lock.jsonc +LICENCE diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 00000000..3c1774d2 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,41 @@ +# DCP CLI + +Dev tool for previewing prompt outputs. Verify parsing works correctly and quickly check specific tool combinations. + +## Usage + +```bash +bun run dcp [TYPE] [-d] [-e] [-s] +``` + +## Types + +| Flag | Description | +| ------------------ | --------------------------- | +| `--system` | System prompt | +| `--nudge` | Nudge prompt | +| `--prune-list` | Example prunable tools list | +| `--squash-context` | Example squash context | + +## Tool Flags + +| Flag | Description | +| --------------- | ------------------- | +| `-d, --discard` | Enable discard tool | +| `-e, --extract` | Enable extract tool | +| `-s, --squash` | Enable squash tool | + +If no tool flags specified, all are enabled. + +## Examples + +```bash +bun run dcp --system -d -e -s # System prompt with all tools +bun run dcp --system -d # System prompt with discard only +bun run dcp --nudge -e -s # Nudge with extract and squash +bun run dcp --prune-list # Example prunable tools list +``` + +## Purpose + +This CLI does NOT ship with the plugin. It's purely for DX - iterate on prompt templates and verify the `` conditional parsing produces the expected output. diff --git a/cli/print.ts b/cli/print.ts new file mode 100644 index 00000000..94ae2ad5 --- /dev/null +++ b/cli/print.ts @@ -0,0 +1,117 @@ +#!/usr/bin/env npx tsx + +import { renderSystemPrompt, renderNudge, type ToolFlags } from "../lib/prompts/index.js" +import { + wrapPrunableTools, + wrapSquashContext, + wrapCooldownMessage, +} from "../lib/messages/inject.js" + +const args = process.argv.slice(2) + +const flags: ToolFlags = { + discard: args.includes("-d") || args.includes("--discard"), + extract: args.includes("-e") || args.includes("--extract"), + squash: args.includes("-s") || args.includes("--squash"), +} + +// Default to all enabled if none specified +if (!flags.discard && !flags.extract && !flags.squash) { + flags.discard = true + flags.extract = true + flags.squash = true +} + +const showSystem = args.includes("--system") +const showNudge = args.includes("--nudge") +const showPruneList = args.includes("--prune-list") +const showSquashContext = args.includes("--squash-context") +const showCooldown = args.includes("--cooldown") +const showHelp = args.includes("--help") || args.includes("-h") + +if ( + showHelp || + (!showSystem && !showNudge && !showPruneList && !showSquashContext && !showCooldown) +) { + console.log(` +Usage: bun run dcp [TYPE] [-d] [-e] [-s] + +Types: + --system System prompt + --nudge Nudge prompt + --prune-list Example prunable tools list + --squash-context Example squash context + --cooldown Cooldown message after pruning + +Tool flags (for --system and --nudge): + -d, --discard Enable discard tool + -e, --extract Enable extract tool + -s, --squash Enable squash tool + +If no tool flags specified, all are enabled. + +Examples: + bun run dcp --system -d -e -s # System prompt with all tools + bun run dcp --system -d # System prompt with discard only + bun run dcp --nudge -e -s # Nudge with extract and squash + bun run dcp --prune-list # Example prunable tools list +`) + process.exit(0) +} + +const header = (title: string) => { + console.log() + console.log("─".repeat(60)) + console.log(title) + console.log("─".repeat(60)) +} + +if (showSystem) { + const enabled = [ + flags.discard && "discard", + flags.extract && "extract", + flags.squash && "squash", + ] + .filter(Boolean) + .join(", ") + header(`SYSTEM PROMPT (tools: ${enabled})`) + console.log(renderSystemPrompt(flags)) +} + +if (showNudge) { + const enabled = [ + flags.discard && "discard", + flags.extract && "extract", + flags.squash && "squash", + ] + .filter(Boolean) + .join(", ") + header(`NUDGE (tools: ${enabled})`) + console.log(renderNudge(flags)) +} + +if (showPruneList) { + header("PRUNABLE TOOLS LIST (mock example)") + const mockList = `5: read, /path/to/file.ts +8: bash, npm run build +12: glob, src/**/*.ts +15: read, /path/to/another-file.ts` + console.log(wrapPrunableTools(mockList)) +} + +if (showSquashContext) { + header("SQUASH CONTEXT (mock example)") + console.log(wrapSquashContext(45)) +} + +if (showCooldown) { + const enabled = [ + flags.discard && "discard", + flags.extract && "extract", + flags.squash && "squash", + ] + .filter(Boolean) + .join(", ") + header(`COOLDOWN MESSAGE (tools: ${enabled})`) + console.log(wrapCooldownMessage(flags)) +} diff --git a/lib/hooks.ts b/lib/hooks.ts index eee5801b..126fec34 100644 --- a/lib/hooks.ts +++ b/lib/hooks.ts @@ -5,7 +5,7 @@ import { syncToolCache } from "./state/tool-cache" import { deduplicate, supersedeWrites, purgeErrors } from "./strategies" import { prune, insertPruneToolContext } from "./messages" import { checkSession } from "./state" -import { loadPrompt } from "./prompts" +import { renderSystemPrompt } from "./prompts" import { handleStatsCommand } from "./commands/stats" import { handleContextCommand } from "./commands/context" import { handleHelpCommand } from "./commands/help" @@ -33,31 +33,17 @@ export function createSystemPromptHandler( return } - const discardEnabled = config.tools.discard.enabled - const extractEnabled = config.tools.extract.enabled - const squashEnabled = config.tools.squash.enabled - - let promptName: string - if (discardEnabled && extractEnabled && squashEnabled) { - promptName = "system/system-prompt-all" - } else if (discardEnabled && extractEnabled) { - promptName = "system/system-prompt-discard-extract" - } else if (discardEnabled && squashEnabled) { - promptName = "system/system-prompt-discard-squash" - } else if (extractEnabled && squashEnabled) { - promptName = "system/system-prompt-extract-squash" - } else if (discardEnabled) { - promptName = "system/system-prompt-discard" - } else if (extractEnabled) { - promptName = "system/system-prompt-extract" - } else if (squashEnabled) { - promptName = "system/system-prompt-squash" - } else { + const flags = { + discard: config.tools.discard.enabled, + extract: config.tools.extract.enabled, + squash: config.tools.squash.enabled, + } + + if (!flags.discard && !flags.extract && !flags.squash) { return } - const syntheticPrompt = loadPrompt(promptName) - output.system.push(syntheticPrompt) + output.system.push(renderSystemPrompt(flags)) } } diff --git a/lib/messages/inject.ts b/lib/messages/inject.ts index 91761393..cc7ec771 100644 --- a/lib/messages/inject.ts +++ b/lib/messages/inject.ts @@ -2,7 +2,7 @@ import type { SessionState, WithParts } from "../state" import type { Logger } from "../logger" import type { PluginConfig } from "../config" import type { UserMessage } from "@opencode-ai/sdk/v2" -import { loadPrompt } from "../prompts" +import { renderNudge } from "../prompts" import { extractParameterKey, buildToolIdList, @@ -15,43 +15,26 @@ import { import { getFilePathFromParameters, isProtectedFilePath } from "../protected-file-patterns" import { getLastUserMessage, isMessageCompacted } from "../shared-utils" -const getNudgeString = (config: PluginConfig): string => { - const discardEnabled = config.tools.discard.enabled - const extractEnabled = config.tools.extract.enabled - const squashEnabled = config.tools.squash.enabled - - if (discardEnabled && extractEnabled && squashEnabled) { - return loadPrompt(`nudge/nudge-all`) - } else if (discardEnabled && extractEnabled) { - return loadPrompt(`nudge/nudge-discard-extract`) - } else if (discardEnabled && squashEnabled) { - return loadPrompt(`nudge/nudge-discard-squash`) - } else if (extractEnabled && squashEnabled) { - return loadPrompt(`nudge/nudge-extract-squash`) - } else if (discardEnabled) { - return loadPrompt(`nudge/nudge-discard`) - } else if (extractEnabled) { - return loadPrompt(`nudge/nudge-extract`) - } else if (squashEnabled) { - return loadPrompt(`nudge/nudge-squash`) - } - return "" -} - -const wrapPrunableTools = (content: string): string => ` +export const wrapPrunableTools = (content: string): string => ` The 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 pruning valuable tool inputs or outputs. Consolidate your prunes for efficiency; it is rarely worth pruning a single tiny tool output. Keep the context free of noise. ${content} ` -const getCooldownMessage = (config: PluginConfig): string => { - const discardEnabled = config.tools.discard.enabled - const extractEnabled = config.tools.extract.enabled - const squashEnabled = config.tools.squash.enabled +export const wrapSquashContext = (messageCount: number): string => ` +Squash available. Conversation: ${messageCount} messages. +Squash collapses completed task sequences or exploration phases into summaries. +Uses text boundaries [startString, endString, topic, summary]. +` +export const wrapCooldownMessage = (flags: { + discard: boolean + extract: boolean + squash: boolean +}): string => { const enabledTools: string[] = [] - if (discardEnabled) enabledTools.push("discard") - if (extractEnabled) enabledTools.push("extract") - if (squashEnabled) enabledTools.push("squash") + if (flags.discard) enabledTools.push("discard") + if (flags.extract) enabledTools.push("extract") + if (flags.squash) enabledTools.push("squash") let toolName: string if (enabledTools.length === 0) { @@ -64,18 +47,35 @@ const getCooldownMessage = (config: PluginConfig): string => { } return ` -Context management was just performed. Do not use the ${toolName} again. A fresh list will be available after your next tool use. +Context management was just performed. Do NOT use the ${toolName} again. A fresh list will be available after your next tool use. ` } +const getNudgeString = (config: PluginConfig): string => { + const flags = { + discard: config.tools.discard.enabled, + extract: config.tools.extract.enabled, + squash: config.tools.squash.enabled, + } + + if (!flags.discard && !flags.extract && !flags.squash) { + return "" + } + + return renderNudge(flags) +} + +const getCooldownMessage = (config: PluginConfig): string => { + return wrapCooldownMessage({ + discard: config.tools.discard.enabled, + extract: config.tools.extract.enabled, + squash: config.tools.squash.enabled, + }) +} + const buildSquashContext = (state: SessionState, messages: WithParts[]): string => { const messageCount = messages.filter((msg) => !isMessageCompacted(state, msg)).length - - return ` -Squash available. Conversation: ${messageCount} messages. -Squash collapses completed task sequences or exploration phases into summaries. -Uses text boundaries [startString, endString, topic, summary]. -` + return wrapSquashContext(messageCount) } const buildPrunableToolsList = ( diff --git a/lib/prompts/index.ts b/lib/prompts/index.ts index 8a6bf745..a9d70b10 100644 --- a/lib/prompts/index.ts +++ b/lib/prompts/index.ts @@ -1,44 +1,50 @@ +import { readFileSync } from "node:fs" +import { dirname, join } from "node:path" +import { fileURLToPath } from "node:url" + // Tool specs import { DISCARD_TOOL_SPEC } from "./discard-tool-spec" import { EXTRACT_TOOL_SPEC } from "./extract-tool-spec" import { SQUASH_TOOL_SPEC } from "./squash-tool-spec" -// System prompts -import { SYSTEM_PROMPT_DISCARD } from "./system/discard" -import { SYSTEM_PROMPT_EXTRACT } from "./system/extract" -import { SYSTEM_PROMPT_SQUASH } from "./system/squash" -import { SYSTEM_PROMPT_DISCARD_EXTRACT } from "./system/discard-extract" -import { SYSTEM_PROMPT_DISCARD_SQUASH } from "./system/discard-squash" -import { SYSTEM_PROMPT_EXTRACT_SQUASH } from "./system/extract-squash" -import { SYSTEM_PROMPT_ALL } from "./system/all" - -// Nudge prompts -import { NUDGE_DISCARD } from "./nudge/discard" -import { NUDGE_EXTRACT } from "./nudge/extract" -import { NUDGE_SQUASH } from "./nudge/squash" -import { NUDGE_DISCARD_EXTRACT } from "./nudge/discard-extract" -import { NUDGE_DISCARD_SQUASH } from "./nudge/discard-squash" -import { NUDGE_EXTRACT_SQUASH } from "./nudge/extract-squash" -import { NUDGE_ALL } from "./nudge/all" +const __dirname = dirname(fileURLToPath(import.meta.url)) + +// Load markdown prompts at module init +const SYSTEM_PROMPT = readFileSync(join(__dirname, "system.md"), "utf-8") +const NUDGE = readFileSync(join(__dirname, "nudge.md"), "utf-8") + +export interface ToolFlags { + discard: boolean + extract: boolean + squash: boolean +} + +function processConditionals(template: string, flags: ToolFlags): string { + const tools = ["discard", "extract", "squash"] as const + let result = template + // Strip comments: // ... // + result = result.replace(/\/\/.*?\/\//g, "") + // Process tool conditionals + for (const tool of tools) { + const regex = new RegExp(`<${tool}>([\\s\\S]*?)`, "g") + result = result.replace(regex, (_, content) => (flags[tool] ? content : "")) + } + // Collapse multiple blank/whitespace-only lines to single blank line + return result.replace(/\n([ \t]*\n)+/g, "\n\n").trim() +} + +export function renderSystemPrompt(flags: ToolFlags): string { + return processConditionals(SYSTEM_PROMPT, flags) +} + +export function renderNudge(flags: ToolFlags): string { + return processConditionals(NUDGE, flags) +} const PROMPTS: Record = { "discard-tool-spec": DISCARD_TOOL_SPEC, "extract-tool-spec": EXTRACT_TOOL_SPEC, "squash-tool-spec": SQUASH_TOOL_SPEC, - "system/system-prompt-discard": SYSTEM_PROMPT_DISCARD, - "system/system-prompt-extract": SYSTEM_PROMPT_EXTRACT, - "system/system-prompt-squash": SYSTEM_PROMPT_SQUASH, - "system/system-prompt-discard-extract": SYSTEM_PROMPT_DISCARD_EXTRACT, - "system/system-prompt-discard-squash": SYSTEM_PROMPT_DISCARD_SQUASH, - "system/system-prompt-extract-squash": SYSTEM_PROMPT_EXTRACT_SQUASH, - "system/system-prompt-all": SYSTEM_PROMPT_ALL, - "nudge/nudge-discard": NUDGE_DISCARD, - "nudge/nudge-extract": NUDGE_EXTRACT, - "nudge/nudge-squash": NUDGE_SQUASH, - "nudge/nudge-discard-extract": NUDGE_DISCARD_EXTRACT, - "nudge/nudge-discard-squash": NUDGE_DISCARD_SQUASH, - "nudge/nudge-extract-squash": NUDGE_EXTRACT_SQUASH, - "nudge/nudge-all": NUDGE_ALL, } export function loadPrompt(name: string, vars?: Record): string { diff --git a/lib/prompts/nudge.md b/lib/prompts/nudge.md new file mode 100644 index 00000000..c7e835fc --- /dev/null +++ b/lib/prompts/nudge.md @@ -0,0 +1,16 @@ + +**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. + +**Immediate Actions Required:** + +**Phase Completion:** If a phase is complete, use the `squash` tool to condense the entire sequence into a summary. + + +**Noise Removal:** If you read files or ran commands that yielded no value, use the `discard` tool to remove them. If older outputs have been replaced by newer ones, discard the outdated versions. + + +**Knowledge Preservation:** If you are holding valuable raw data you'll need to reference later, use the `extract` tool with high-fidelity distillation to preserve the insights and remove the raw entry. + + +**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must perform context management. + diff --git a/lib/prompts/nudge/all.ts b/lib/prompts/nudge/all.ts deleted file mode 100644 index 08e86e8f..00000000 --- a/lib/prompts/nudge/all.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const NUDGE_ALL = ` -**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. - -**Immediate Actions Required:** -1. **Phase Completion:** If a phase is complete, use the \`squash\` tool to condense the entire sequence into a summary. -2. **Noise Removal:** If you read files or ran commands that yielded no value, use \`discard\` to remove them. -3. **Knowledge Preservation:** If you are holding valuable raw data you'll need to reference later, use \`extract\` to distill the insights and remove the raw entry. - -**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must perform context management. -` diff --git a/lib/prompts/nudge/discard-extract.ts b/lib/prompts/nudge/discard-extract.ts deleted file mode 100644 index 2e1b8615..00000000 --- a/lib/prompts/nudge/discard-extract.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const NUDGE_DISCARD_EXTRACT = ` -**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. - -**Immediate Actions Required:** -1. **Noise Removal:** If you read files or ran commands that yielded no value, use \`discard\` to remove them. -2. **Superseded Info:** If older outputs have been replaced by newer ones, use \`discard\` on the outdated versions. -3. **Knowledge Preservation:** If you have large outputs with valuable technical details, use \`extract\` to distill the insights and remove the raw entry. - -**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must perform context management. -` diff --git a/lib/prompts/nudge/discard-squash.ts b/lib/prompts/nudge/discard-squash.ts deleted file mode 100644 index 699a716f..00000000 --- a/lib/prompts/nudge/discard-squash.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const NUDGE_DISCARD_SQUASH = ` -**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. - -**Immediate Actions Required:** -1. **Phase Completion:** If a phase is complete, use the \`squash\` tool to condense the entire sequence into a summary. -2. **Noise Removal:** If you read files or ran commands that yielded no value, use the \`discard\` tool to remove them. - -**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must perform context management. -` diff --git a/lib/prompts/nudge/discard.ts b/lib/prompts/nudge/discard.ts deleted file mode 100644 index 13e6314b..00000000 --- a/lib/prompts/nudge/discard.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const NUDGE_DISCARD = ` -**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. - -**Immediate Actions Required:** -1. **Noise Removal:** If you read files or ran commands that yielded no value, use the \`discard\` tool to remove them. -2. **Superseded Info:** If older outputs have been replaced by newer ones, discard the outdated versions. - -**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must discard unneeded tool outputs. -` diff --git a/lib/prompts/nudge/extract-squash.ts b/lib/prompts/nudge/extract-squash.ts deleted file mode 100644 index 88053e80..00000000 --- a/lib/prompts/nudge/extract-squash.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const NUDGE_EXTRACT_SQUASH = ` -**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. - -**Immediate Actions Required:** -1. **Phase Completion:** If a phase is complete, use the \`squash\` tool to condense the entire sequence into a summary. -2. **Knowledge Preservation:** If you are holding valuable raw data you'll need to reference later, use \`extract\` to distill the insights and remove the raw entry. - -**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must perform context management. -` diff --git a/lib/prompts/nudge/extract.ts b/lib/prompts/nudge/extract.ts deleted file mode 100644 index 16ea5b78..00000000 --- a/lib/prompts/nudge/extract.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const NUDGE_EXTRACT = ` -**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. - -**Immediate Actions Required:** -1. **Large Outputs:** If you have large tool outputs with valuable technical details, use the \`extract\` tool to distill and preserve key information. -2. **Knowledge Preservation:** If you are holding valuable raw data you'll need to reference later, use the \`extract\` tool with high-fidelity distillation to preserve the insights and remove the raw entry. - -**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must extract valuable findings from tool outputs. -` diff --git a/lib/prompts/nudge/squash.ts b/lib/prompts/nudge/squash.ts deleted file mode 100644 index ba4c9097..00000000 --- a/lib/prompts/nudge/squash.ts +++ /dev/null @@ -1,9 +0,0 @@ -export const NUDGE_SQUASH = ` -**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. - -**Immediate Actions Required:** -1. **Phase Completion:** If a phase is complete, use the \`squash\` tool to condense the entire sequence into a summary. -2. **Exploration Done:** If you explored multiple files or ran multiple commands, squash the results to focus on the next phase. - -**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must squash completed conversation ranges. -` diff --git a/lib/prompts/system.md b/lib/prompts/system.md new file mode 100644 index 00000000..b4671022 --- /dev/null +++ b/lib/prompts/system.md @@ -0,0 +1,91 @@ + + + +ENVIRONMENT +You are operating in a context-constrained environment and must proactively manage your context window. The environment calls the `context_info` tool to provide an up-to-date list after each turn. Use this information when deciding what to prune. + +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. + +AVAILABLE TOOLS + +`discard`: Remove individual tool outputs that are noise, irrelevant, or superseded. No preservation of content. + + +`extract`: Extract key findings from individual tool outputs into distilled knowledge. Use when you need to preserve valuable technical details. + + +`squash`: Collapse a contiguous range of conversation (completed phases) into a single summary. + + +PRUNE METHODICALLY - BATCH YOUR ACTIONS +Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by pruning. Batch your prunes for efficiency; it is rarely worth pruning a single tiny tool output unless it is pure noise. Evaluate what SHOULD be pruned before jumping the gun. + +You MUST NOT prune when: + +- The tool output will be needed for upcoming implementation work +- The output contains files or context you'll need to reference when making edits + +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. + + +WHEN TO DISCARD +- **Noise Removal:** Outputs that are irrelevant, unhelpful, or superseded by newer info. +- **Wrong Files:** You read or accessed something that turned out to be irrelevant to the current work. +- **Outdated Info:** Outputs that have been superseded by newer information. + +You WILL evaluate discarding when ANY of these are true: + +You accessed something that turned out to be irrelevant +Information has been superseded by newer outputs +You are about to start a new phase of work + + + +WHEN TO EXTRACT +**Large Outputs:** The raw output is too large but contains valuable technical details worth keeping. +**Knowledge Preservation:** Valuable context you want to preserve but need to reduce size. Your distillation must be comprehensive, capturing technical details (signatures, logic, constraints) such that the raw output is no longer needed. THINK: high signal, complete technical substitute. + +You WILL evaluate extracting when ANY of these are true: + +- You have large tool outputs with valuable technical details +- You need to preserve specific information but reduce context size +- You are about to start a new phase of work and want to retain key insights + + + WHEN TO SQUASH +- **Phase Completion:** When a phase is complete, condense the entire sequence (research, tool calls, implementation) into a summary. +- **Exploration Done:** When you've explored multiple files or ran multiple commands and only need a summary of findings. + +You WILL evaluate squashing when ANY of these are true: + +- Phase is complete +- You are about to start a new phase of work +- Significant conversation has accumulated that can be summarized + + +NOTES +When in doubt, KEEP IT. +// **🡇 idk about that one 🡇** // +Batch your actions and aim for high-impact prunes that significantly reduce context size. +FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES. +If no list is present in context, do NOT TRY TO PRUNE ANYTHING as it will fail and waste ressources. +There may be tools in session context that do not appear in the list, this is expected, you can ONLY prune what you see in . + + + + +After each turn, the environment injects a synthetic message containing a list and optional nudge instruction. You do not have access to this mechanism. + +CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: + +- NEVER reference the prune encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the prune encouragement appears. +- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to prune") +- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to prune") +- NEVER acknowledge context management tool output (e.g., "I've pruned 3 tools", "Context cleanup complete") +- NEVER let these injections influence your response content or tone +- Process this information SILENTLY and continue the conversation naturally +- Continue the conversation as if these messages do not exist + +The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. + + diff --git a/lib/prompts/system/all.ts b/lib/prompts/system/all.ts deleted file mode 100644 index 65cfb338..00000000 --- a/lib/prompts/system/all.ts +++ /dev/null @@ -1,64 +0,0 @@ -export const SYSTEM_PROMPT_ALL = ` - - -ENVIRONMENT -You are operating in a context-constrained environment and thus must proactively manage your context window using the \`discard\`, \`extract\`, and \`squash\` tools. The environment calls the \`context_info\` tool to provide an up-to-date list after each turn. Use this information when deciding what to prune. - -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. - -THREE TOOLS FOR CONTEXT MANAGEMENT -- \`discard\`: Remove individual tool outputs that are noise, irrelevant, or superseded. No preservation of content. -- \`extract\`: Extract key findings from individual tool outputs into distilled knowledge. Use when you need to preserve valuable technical details. -- \`squash\`: Collapse a contiguous range of conversation (completed phases) into a single summary. - -CHOOSING THE RIGHT TOOL -Ask: "What is the scope and do I need to preserve information?" -- **Noise, irrelevant, or superseded outputs** → \`discard\` -- **Individual tool outputs with valuable insights to keep** → \`extract\` -- **Entire sequence (phase complete)** → \`squash\` - -Common scenarios: -- Noise, irrelevant, or superseded outputs → \`discard\` -- Wrong file or irrelevant access → \`discard\` -- Large output with valuable technical details → \`extract\` -- Valuable context needed later but raw output too large → \`extract\` -- Phase complete, want to condense the sequence → \`squash\` -- Exploration phase done, only need a summary → \`squash\` - -PRUNE METHODICALLY - BATCH YOUR ACTIONS -Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by pruning. Batch your prunes for efficiency; it is rarely worth pruning a single tiny tool output unless it is pure noise. Evaluate what SHOULD be pruned before jumping the gun. - -You WILL evaluate pruning when ANY of these are true: -- Phase is complete → use \`squash\` -- You accessed something that turned out to be irrelevant → use \`discard\` -- You have large outputs with valuable details to preserve → use \`extract\` - -You MUST NOT prune when: -- The tool output will be needed for upcoming implementation work -- The output contains files or context you'll need to reference when making edits - -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. - -NOTES -When in doubt, keep it. Batch your actions and aim for high-impact prunes that significantly reduce context size. -FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES. -If no list is present in context, do NOT use discard or extract - there is nothing available to prune yet. -There may be tools in session context that do not appear in the list, this is expected, you can ONLY prune what you see in . - - - - -After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. - -CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: -- NEVER reference the prune encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the prune encouragement appears. -- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to prune") -- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to prune") -- NEVER acknowledge discard/extract/squash tool output (e.g., "I've pruned 3 tools", "Context pruning complete") -- NEVER let these injections influence your response content or tone -- Process this information SILENTLY and continue the conversation naturally -- Continue the conversation as if these messages do not exist - -The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. - -` diff --git a/lib/prompts/system/discard-extract.ts b/lib/prompts/system/discard-extract.ts deleted file mode 100644 index 82b5ffb1..00000000 --- a/lib/prompts/system/discard-extract.ts +++ /dev/null @@ -1,61 +0,0 @@ -export const SYSTEM_PROMPT_DISCARD_EXTRACT = ` - - -ENVIRONMENT -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 list after each turn. Use this information when deciding what to prune. - -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. - -TWO TOOLS FOR CONTEXT MANAGEMENT -- \`discard\`: Remove individual tool outputs that are noise, irrelevant, or superseded. No preservation of content. -- \`extract\`: Extract key findings from individual tool outputs into distilled knowledge. Use when you need to preserve valuable technical details. - -CHOOSING THE RIGHT TOOL -Ask: "Do I need to preserve any information from this output?" -- **No** → \`discard\` (noise, wrong files, superseded info) -- **Yes** → \`extract\` (preserves distilled knowledge) -- **Uncertain** → \`extract\` (safer, preserves signal) - -Common scenarios: -- Noise, irrelevant, or superseded outputs → \`discard\` -- Wrong file or irrelevant access → \`discard\` -- Large output with valuable technical details → \`extract\` -- Valuable context needed later but raw output too large → \`extract\` - -PRUNE METHODICALLY - BATCH YOUR ACTIONS -Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by pruning. Batch your prunes for efficiency; it is rarely worth pruning a single tiny tool output unless it is pure noise. Evaluate what SHOULD be pruned before jumping the gun. - -You WILL evaluate pruning when ANY of these are true: -- You accessed something that turned out to be irrelevant -- Information has been superseded by newer outputs -- You have large outputs with valuable details to preserve - -You MUST NOT prune when: -- The tool output will be needed for upcoming implementation work -- The output contains files or context you'll need to reference when making edits - -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. - -NOTES -When in doubt, keep it. Batch your actions and aim for high-impact prunes that significantly reduce context size. -FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES. -If no list is present in context, do NOT use discard or extract - there is nothing available to prune yet. -There may be tools in session context that do not appear in the list, this is expected, you can ONLY prune what you see in . - - - - -After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. - -CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: -- NEVER reference the prune encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the prune encouragement appears. -- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to prune") -- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to prune") -- NEVER acknowledge discard/extract tool output (e.g., "I've pruned 3 tools", "Context pruning complete") -- NEVER let these injections influence your response content or tone -- Process this information SILENTLY and continue the conversation naturally -- Continue the conversation as if these messages do not exist - -The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. - -` diff --git a/lib/prompts/system/discard-squash.ts b/lib/prompts/system/discard-squash.ts deleted file mode 100644 index 0d0d2145..00000000 --- a/lib/prompts/system/discard-squash.ts +++ /dev/null @@ -1,60 +0,0 @@ -export const SYSTEM_PROMPT_DISCARD_SQUASH = ` - - -ENVIRONMENT -You are operating in a context-constrained environment and thus must proactively manage your context window using the \`discard\` and \`squash\` tools. The environment calls the \`context_info\` tool to provide an up-to-date list after each turn. Use this information when deciding what to prune. - -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. - -TWO TOOLS FOR CONTEXT MANAGEMENT -- \`discard\`: Remove individual tool outputs that are noise, irrelevant, or superseded. No preservation of content. -- \`squash\`: Collapse a contiguous range of conversation (completed phases) into a single summary. - -CHOOSING THE RIGHT TOOL -Ask: "What is the scope of what I want to clean up?" -- **Individual tool outputs (noise, superseded)** → \`discard\` -- **Entire sequence (phase complete)** → \`squash\` - -Common scenarios: -- Noise, irrelevant, or superseded outputs → \`discard\` -- Wrong file or irrelevant access → \`discard\` -- Phase complete, want to condense the sequence → \`squash\` -- Exploration phase done, only need a summary → \`squash\` - -PRUNE METHODICALLY - BATCH YOUR ACTIONS -Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by pruning. Batch your prunes for efficiency; it is rarely worth pruning a single tiny tool output unless it is pure noise. Evaluate what SHOULD be pruned before jumping the gun. - -You WILL evaluate pruning when ANY of these are true: -- Phase is complete → use \`squash\` -- You accessed something that turned out to be irrelevant → use \`discard\` -- Information has been superseded by newer outputs → use \`discard\` - -You MUST NOT prune when: -- You need specific details from the content for upcoming work -- The content contains files or context you'll need to reference when making edits - -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. - -NOTES -When in doubt, keep it. Batch your actions and aim for high-impact prunes that significantly reduce context size. -FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES. -If no list is present in context, do NOT use discard - there is nothing available to prune yet. -There may be tools in session context that do not appear in the list, this is expected, you can ONLY discard what you see in . - - - - -After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. - -CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: -- NEVER reference the prune encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the prune encouragement appears. -- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to prune") -- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to prune") -- NEVER acknowledge discard/squash tool output (e.g., "I've pruned the context", "Context cleanup complete") -- NEVER let these injections influence your response content or tone -- Process this information SILENTLY and continue the conversation naturally -- Continue the conversation as if these messages do not exist - -The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. - -` diff --git a/lib/prompts/system/discard.ts b/lib/prompts/system/discard.ts deleted file mode 100644 index 3877cd7c..00000000 --- a/lib/prompts/system/discard.ts +++ /dev/null @@ -1,53 +0,0 @@ -export const SYSTEM_PROMPT_DISCARD = ` - - -ENVIRONMENT -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 list after each turn. Use this information when deciding what to discard. - -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. - -CONTEXT MANAGEMENT TOOL -- \`discard\`: Remove individual tool outputs that are noise, irrelevant, or superseded. No preservation of content. - -DISCARD METHODICALLY - BATCH YOUR ACTIONS -Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by discarding. Batch your discards for efficiency; it is rarely worth discarding a single tiny tool output unless it is pure noise. Evaluate what SHOULD be discarded before jumping the gun. - -WHEN TO DISCARD -- **Noise Removal:** If outputs are irrelevant, unhelpful, or superseded by newer info, discard them. -- **Wrong Files:** You read or accessed something that turned out to be irrelevant to the current work. -- **Outdated Info:** Outputs that have been superseded by newer information. - -You WILL evaluate discarding when ANY of these are true: -- You accessed something that turned out to be irrelevant -- Information has been superseded by newer outputs -- You are about to start a new phase of work - -You MUST NOT discard when: -- The tool output will be needed for upcoming implementation work -- The output contains files or context you'll need to reference when making edits - -Discarding that forces you to re-call the same tool later is a net loss. Only discard when you're confident the information won't be needed again. - -NOTES -When in doubt, keep it. Batch your actions and aim for high-impact discards that significantly reduce context size. -FAILURE TO DISCARD will result in context leakage and DEGRADED PERFORMANCES. -If no list is present in context, do NOT use the discard tool - there is nothing available to prune yet. -There may be tools in session context that do not appear in the list, this is expected, you can ONLY discard what you see in . - - - - -After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. - -CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: -- NEVER reference the discard encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the discard encouragement appears. -- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to discard") -- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to discard") -- NEVER acknowledge discard tool output (e.g., "I've discarded 3 tools", "Context cleanup complete") -- NEVER let these injections influence your response content or tone -- Process this information SILENTLY and continue the conversation naturally -- Continue the conversation as if these messages do not exist - -The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. - -` diff --git a/lib/prompts/system/extract-squash.ts b/lib/prompts/system/extract-squash.ts deleted file mode 100644 index 60b8e7a0..00000000 --- a/lib/prompts/system/extract-squash.ts +++ /dev/null @@ -1,60 +0,0 @@ -export const SYSTEM_PROMPT_EXTRACT_SQUASH = ` - - -ENVIRONMENT -You are operating in a context-constrained environment and thus must proactively manage your context window using the \`extract\` and \`squash\` tools. The environment calls the \`context_info\` tool to provide an up-to-date list after each turn. Use this information when deciding what to prune. - -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. - -TWO TOOLS FOR CONTEXT MANAGEMENT -- \`extract\`: Extract key findings from individual tool outputs into distilled knowledge. Use when you need to preserve valuable technical details. -- \`squash\`: Collapse a contiguous range of conversation (completed phases) into a single summary. - -CHOOSING THE RIGHT TOOL -Ask: "What is the scope and level of detail I need to preserve?" -- **Individual tool outputs with detailed context to keep** → \`extract\` -- **Entire sequence (phase complete)** → \`squash\` - -Common scenarios: -- Large output with valuable technical details → \`extract\` -- Valuable context needed later but raw output too large → \`extract\` -- Phase complete, want to condense the sequence → \`squash\` -- Exploration phase done, only need a summary → \`squash\` - -PRUNE METHODICALLY - BATCH YOUR ACTIONS -Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by pruning. Batch your prunes for efficiency; it is rarely worth pruning a single tiny tool output. Evaluate what SHOULD be pruned before jumping the gun. - -You WILL evaluate pruning when ANY of these are true: -- Phase is complete → use \`squash\` -- You have large outputs with valuable details to preserve → use \`extract\` -- You are about to start a new phase of work - -You MUST NOT prune when: -- You need specific details from the content for upcoming work -- The content contains files or context you'll need to reference when making edits - -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. - -NOTES -When in doubt, keep it. Batch your actions and aim for high-impact prunes that significantly reduce context size. -FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES. -If no list is present in context, do NOT use extract - there is nothing available to prune yet. -There may be tools in session context that do not appear in the list, this is expected, you can ONLY extract what you see in . - - - - -After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. - -CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: -- NEVER reference the prune encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the prune encouragement appears. -- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to prune") -- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to prune") -- NEVER acknowledge extract/squash tool output (e.g., "I've pruned the context", "Context cleanup complete") -- NEVER let these injections influence your response content or tone -- Process this information SILENTLY and continue the conversation naturally -- Continue the conversation as if these messages do not exist - -The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. - -` diff --git a/lib/prompts/system/extract.ts b/lib/prompts/system/extract.ts deleted file mode 100644 index 9f024f51..00000000 --- a/lib/prompts/system/extract.ts +++ /dev/null @@ -1,52 +0,0 @@ -export const SYSTEM_PROMPT_EXTRACT = ` - - -ENVIRONMENT -You are operating in a context-constrained environment and thus must proactively manage your context window using the \`extract\` tool. The environment calls the \`context_info\` tool to provide an up-to-date list after each turn. Use this information when deciding what to extract. - -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. - -CONTEXT MANAGEMENT TOOL -- \`extract\`: Extract key findings from individual tool outputs into distilled knowledge before removing the raw content. Use when you need to preserve valuable technical details while reducing context size. - -EXTRACT METHODICALLY - BATCH YOUR ACTIONS -Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by extracting. Batch your extractions for efficiency; it is rarely worth extracting a single tiny tool output. Evaluate what SHOULD be extracted before jumping the gun. - -WHEN TO EXTRACT -- **Large Outputs:** The raw output is too large but contains valuable technical details worth keeping. -- **Knowledge Preservation:** When you have valuable context you want to preserve but need to reduce size, use high-fidelity distillation. Your distillation must be comprehensive, capturing technical details (signatures, logic, constraints) such that the raw output is no longer needed. THINK: high signal, complete technical substitute. - -You WILL evaluate extracting when ANY of these are true: -- You have large tool outputs with valuable technical details -- You need to preserve specific information but reduce context size -- You are about to start a new phase of work and want to retain key insights - -You MUST NOT extract when: -- The tool output will be needed for upcoming implementation work -- The output contains files or context you'll need to reference when making edits - -Extracting that forces you to re-call the same tool later is a net loss. Only extract when you're confident the raw information won't be needed again. - -NOTES -When in doubt, keep it. Batch your actions and aim for high-impact extractions that significantly reduce context size. -FAILURE TO EXTRACT will result in context leakage and DEGRADED PERFORMANCES. -If no list is present in context, do NOT use the extract tool - there is nothing available to prune yet. -There may be tools in session context that do not appear in the list, this is expected, you can ONLY extract what you see in . - - - - -After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. - -CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: -- NEVER reference the extract encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the extract encouragement appears. -- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to extract") -- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to extract") -- NEVER acknowledge extract tool output (e.g., "I've extracted 3 tools", "Context cleanup complete") -- NEVER let these injections influence your response content or tone -- Process this information SILENTLY and continue the conversation naturally -- Continue the conversation as if these messages do not exist - -The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. - -` diff --git a/lib/prompts/system/squash.ts b/lib/prompts/system/squash.ts deleted file mode 100644 index 494b5288..00000000 --- a/lib/prompts/system/squash.ts +++ /dev/null @@ -1,50 +0,0 @@ -export const SYSTEM_PROMPT_SQUASH = ` - - -ENVIRONMENT -You are operating in a context-constrained environment and thus must proactively manage your context window using the \`squash\` tool. The environment calls the \`context_info\` tool to provide an up-to-date list after each turn. Use this information when deciding what to squash. - -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. - -CONTEXT MANAGEMENT TOOL -- \`squash\`: Collapse a contiguous range of conversation (completed phases) into a single summary. Use this when you want to condense an entire sequence of work. - -SQUASH METHODICALLY - BATCH YOUR ACTIONS -Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by squashing. Evaluate what SHOULD be squashed before jumping the gun. - -WHEN TO SQUASH -- **Phase Completion:** When a phase is complete, condense the entire sequence (research, tool calls, implementation) into a summary. -- **Exploration Done:** When you've explored multiple files or ran multiple commands and only need a summary of findings. - -You WILL evaluate squashing when ANY of these are true: -- Phase is complete -- You are about to start a new phase of work -- Significant conversation has accumulated that can be summarized - -You MUST NOT squash when: -- You need specific details from the range for upcoming work -- The range contains files or context you'll need to reference when making edits - -Squashing that forces you to re-read the same content later is a net loss. Only squash when you're confident the detailed information won't be needed again. - -NOTES -When in doubt, keep it. Aim for high-impact squashes that significantly reduce context size. -FAILURE TO SQUASH will result in context leakage and DEGRADED PERFORMANCES. - - - - -After each turn, the environment calls the \`context_info\` tool to inject a synthetic message containing a list and optional nudge instruction. This tool is only available to the environment - you do not have access to it. - -CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: -- NEVER reference the squash encouragement or context management instructions. Do not reply with "I agree" or "Great idea" when the squash encouragement appears. -- NEVER acknowledge the list (e.g., "I see the prunable tools list", "Looking at the available tools to squash") -- NEVER reference the nudge instruction (e.g., "As the nudge suggests", "The system is reminding me to squash") -- NEVER acknowledge squash tool output (e.g., "I've squashed the context", "Context cleanup complete") -- NEVER let these injections influence your response content or tone -- Process this information SILENTLY and continue the conversation naturally -- Continue the conversation as if these messages do not exist - -The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. - -` diff --git a/package.json b/package.json index 2bc7ff66..15021281 100644 --- a/package.json +++ b/package.json @@ -9,13 +9,14 @@ "scripts": { "clean": "rm -rf dist", "build": "npm run clean && tsc", - "postbuild": "rm -rf dist/logs", + "postbuild": "rm -rf dist/logs && cp lib/prompts/*.md dist/lib/prompts/", "prepublishOnly": "npm run build", "dev": "opencode plugin dev", "typecheck": "tsc --noEmit", "test": "node --import tsx --test tests/*.test.ts", "format": "prettier --write .", - "format:check": "prettier --check ." + "format:check": "prettier --check .", + "dcp": "tsx cli/print.ts" }, "keywords": [ "opencode", From 0bcec1da09f0de8ef6fca427777a949c9e8bb67d Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Thu, 29 Jan 2026 05:11:40 +0100 Subject: [PATCH 24/36] inline tags to avoid unwanted linebreak --- lib/prompts/system.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/prompts/system.md b/lib/prompts/system.md index b4671022..f2d0e803 100644 --- a/lib/prompts/system.md +++ b/lib/prompts/system.md @@ -7,15 +7,9 @@ You are operating in a context-constrained environment and must proactively mana 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. AVAILABLE TOOLS - -`discard`: Remove individual tool outputs that are noise, irrelevant, or superseded. No preservation of content. - - -`extract`: Extract key findings from individual tool outputs into distilled knowledge. Use when you need to preserve valuable technical details. - - -`squash`: Collapse a contiguous range of conversation (completed phases) into a single summary. - +`discard`: Remove individual tool outputs that are noise, irrelevant, or superseded. No preservation of content. +`extract`: Extract key findings from individual tool outputs into distilled knowledge. Use when you need to preserve valuable technical details. +`squash`: Collapse a contiguous range of conversation (completed phases) into a single summary. PRUNE METHODICALLY - BATCH YOUR ACTIONS Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by pruning. Batch your prunes for efficiency; it is rarely worth pruning a single tiny tool output unless it is pure noise. Evaluate what SHOULD be pruned before jumping the gun. From c645e691202137237fa6404623214ca323973c65 Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Thu, 29 Jan 2026 06:19:58 +0100 Subject: [PATCH 25/36] nudge --- lib/prompts/nudge.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/prompts/nudge.md b/lib/prompts/nudge.md index c7e835fc..528b858f 100644 --- a/lib/prompts/nudge.md +++ b/lib/prompts/nudge.md @@ -1,16 +1,12 @@ -**CRITICAL CONTEXT WARNING:** Your context window is filling with tool outputs. Strict adherence to context hygiene is required. +CRITICAL CONTEXT WARNING +Your context window is filling with tool. Strict adherence to context hygiene is required. -**Immediate Actions Required:** - -**Phase Completion:** If a phase is complete, use the `squash` tool to condense the entire sequence into a summary. - - -**Noise Removal:** If you read files or ran commands that yielded no value, use the `discard` tool to remove them. If older outputs have been replaced by newer ones, discard the outdated versions. - - -**Knowledge Preservation:** If you are holding valuable raw data you'll need to reference later, use the `extract` tool with high-fidelity distillation to preserve the insights and remove the raw entry. - +PROTOCOL +You should prioritize context management, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must perform context management. -**Protocol:** You should prioritize this cleanup, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must perform context management. +IMMEDIATE ACTION REQUIRED +KNOWLEDGE PRESERVATION: If holding valuable raw data you POTENTIALLY will need in your task, use the `extract` tool. Produce a high-fidelity distillation to preserve insights - be thorough +NOISE REMOVAL: If you read files or ran commands that yielded no value, use the `discard` tool to remove them. If newer tools supersedes older ones, discard the old +PHASE COMPLETION: If a phase is complete, use the `squash` tool to condense the entire sequence into a detailed summary From 06ce131b4b3daf9f9724763fb73bb3bbc2bf6ed6 Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Thu, 29 Jan 2026 06:20:04 +0100 Subject: [PATCH 26/36] gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 9f567cb5..177c17b2 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,5 @@ test-update.ts # Documentation (local development only) docs/ SCHEMA_NOTES.md + +repomix-output.xml \ No newline at end of file From 53e2c1586c8f8a0d5b5706bac5d3924fe631f27e Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Thu, 29 Jan 2026 06:26:53 +0100 Subject: [PATCH 27/36] DCP: Distill Compress Prune --- README.md | 18 +-- cli/README.md | 30 ++--- cli/print.ts | 68 ++++++------ dcp.schema.json | 18 +-- index.ts | 20 ++-- lib/config.ts | 105 +++++++++--------- lib/hooks.ts | 8 +- lib/messages/inject.ts | 62 +++++------ lib/messages/prune.ts | 18 +-- lib/messages/utils.ts | 2 +- ...ash-tool-spec.ts => compress-tool-spec.ts} | 22 ++-- lib/prompts/discard-tool-spec.ts | 39 ------- ...ract-tool-spec.ts => distill-tool-spec.ts} | 20 ++-- lib/prompts/index.ts | 20 ++-- lib/prompts/nudge.md | 6 +- lib/prompts/prune-tool-spec.ts | 39 +++++++ lib/prompts/system.md | 30 ++--- lib/state/persistence.ts | 6 +- lib/state/state.ts | 6 +- lib/state/tool-cache.ts | 8 +- lib/state/types.ts | 4 +- lib/state/utils.ts | 2 +- lib/strategies/index.ts | 2 +- lib/tools/{squash.ts => compress.ts} | 44 ++++---- lib/tools/{extract.ts => distill.ts} | 14 +-- lib/tools/index.ts | 6 +- lib/tools/{discard.ts => prune.ts} | 10 +- lib/tools/utils.ts | 12 +- lib/ui/notification.ts | 12 +- 29 files changed, 329 insertions(+), 322 deletions(-) rename lib/prompts/{squash-tool-spec.ts => compress-tool-spec.ts} (71%) delete mode 100644 lib/prompts/discard-tool-spec.ts rename lib/prompts/{extract-tool-spec.ts => distill-tool-spec.ts} (75%) create mode 100644 lib/prompts/prune-tool-spec.ts rename lib/tools/{squash.ts => compress.ts} (76%) rename lib/tools/{extract.ts => distill.ts} (82%) rename lib/tools/{discard.ts => prune.ts} (72%) diff --git a/README.md b/README.md index 0a63b74c..afd338c3 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,11 @@ DCP uses multiple tools and strategies to reduce context size: ### Tools -**Discard** — Exposes a `discard` tool that the AI can call to remove completed or noisy tool content from context. +**Prune** — Exposes a `prune` tool that the AI can call to remove completed or noisy tool content from context. -**Extract** — Exposes an `extract` tool that the AI can call to distill valuable context into concise summaries before removing the tool content. +**Distill** — Exposes a `distill` tool that the AI can call to distill valuable context into concise summaries before removing the tool content. -**Squash** — Exposes a `squash` tool that the AI can call to collapse a large section of conversation (messages and tools) into a single summary. +**Compress** — Exposes a `compress` tool that the AI can call to collapse a large section of conversation (messages and tools) into a single summary. ### Strategies @@ -60,7 +60,7 @@ DCP uses its own config file: - Global: `~/.config/opencode/dcp.jsonc` (or `dcp.json`), created automatically on first run - Custom config directory: `$OPENCODE_CONFIG_DIR/dcp.jsonc` (or `dcp.json`), if `OPENCODE_CONFIG_DIR` is set -- Project: `.opencode/dcp.jsonc` (or `dcp.json`) in your project’s `.opencode` directory +- Project: `.opencode/dcp.jsonc` (or `dcp.json`) in your project's `.opencode` directory
Default Configuration (click to expand) @@ -99,17 +99,17 @@ DCP uses its own config file: "protectedTools": [], }, // Removes tool content from context without preservation (for completed tasks or noise) - "discard": { + "prune": { "enabled": true, }, // Distills key findings into preserved knowledge before removing raw content - "extract": { + "distill": { "enabled": true, // Show distillation content as an ignored message notification "showDistillation": false, }, // Collapses a range of conversation content into a single summary - "squash": { + "compress": { "enabled": true, // Show summary content as an ignored message notification "showSummary": true, @@ -152,12 +152,12 @@ DCP provides a `/dcp` slash command: ### Turn Protection -When enabled, turn protection prevents tool outputs from being pruned for a configurable number of message turns. This gives the AI time to reference recent tool outputs before they become prunable. Applies to both `discard` and `extract` tools, as well as automatic strategies. +When enabled, turn protection prevents tool outputs from being pruned for a configurable number of message turns. This gives the AI time to reference recent tool outputs before they become prunable. Applies to both `prune` and `distill` tools, as well as automatic strategies. ### Protected Tools By default, these tools are always protected from pruning across all strategies: -`task`, `todowrite`, `todoread`, `discard`, `extract`, `squash`, `batch`, `write`, `edit`, `plan_enter`, `plan_exit` +`task`, `todowrite`, `todoread`, `prune`, `distill`, `compress`, `batch`, `write`, `edit`, `plan_enter`, `plan_exit` The `protectedTools` arrays in each section add to this default list. diff --git a/cli/README.md b/cli/README.md index 3c1774d2..abbd3398 100644 --- a/cli/README.md +++ b/cli/README.md @@ -5,34 +5,34 @@ Dev tool for previewing prompt outputs. Verify parsing works correctly and quick ## Usage ```bash -bun run dcp [TYPE] [-d] [-e] [-s] +bun run dcp [TYPE] [-p] [-d] [-c] ``` ## Types -| Flag | Description | -| ------------------ | --------------------------- | -| `--system` | System prompt | -| `--nudge` | Nudge prompt | -| `--prune-list` | Example prunable tools list | -| `--squash-context` | Example squash context | +| Flag | Description | +| -------------------- | --------------------------- | +| `--system` | System prompt | +| `--nudge` | Nudge prompt | +| `--prune-list` | Example prunable tools list | +| `--compress-context` | Example compress context | ## Tool Flags -| Flag | Description | -| --------------- | ------------------- | -| `-d, --discard` | Enable discard tool | -| `-e, --extract` | Enable extract tool | -| `-s, --squash` | Enable squash tool | +| Flag | Description | +| ---------------- | -------------------- | +| `-p, --prune` | Enable prune tool | +| `-d, --distill` | Enable distill tool | +| `-c, --compress` | Enable compress tool | If no tool flags specified, all are enabled. ## Examples ```bash -bun run dcp --system -d -e -s # System prompt with all tools -bun run dcp --system -d # System prompt with discard only -bun run dcp --nudge -e -s # Nudge with extract and squash +bun run dcp --system -p -d -c # System prompt with all tools +bun run dcp --system -p # System prompt with prune only +bun run dcp --nudge -d -c # Nudge with distill and compress bun run dcp --prune-list # Example prunable tools list ``` diff --git a/cli/print.ts b/cli/print.ts index 94ae2ad5..8d795cd3 100644 --- a/cli/print.ts +++ b/cli/print.ts @@ -3,57 +3,57 @@ import { renderSystemPrompt, renderNudge, type ToolFlags } from "../lib/prompts/index.js" import { wrapPrunableTools, - wrapSquashContext, + wrapCompressContext, wrapCooldownMessage, } from "../lib/messages/inject.js" const args = process.argv.slice(2) const flags: ToolFlags = { - discard: args.includes("-d") || args.includes("--discard"), - extract: args.includes("-e") || args.includes("--extract"), - squash: args.includes("-s") || args.includes("--squash"), + prune: args.includes("-p") || args.includes("--prune"), + distill: args.includes("-d") || args.includes("--distill"), + compress: args.includes("-c") || args.includes("--compress"), } // Default to all enabled if none specified -if (!flags.discard && !flags.extract && !flags.squash) { - flags.discard = true - flags.extract = true - flags.squash = true +if (!flags.prune && !flags.distill && !flags.compress) { + flags.prune = true + flags.distill = true + flags.compress = true } const showSystem = args.includes("--system") const showNudge = args.includes("--nudge") const showPruneList = args.includes("--prune-list") -const showSquashContext = args.includes("--squash-context") +const showCompressContext = args.includes("--compress-context") const showCooldown = args.includes("--cooldown") const showHelp = args.includes("--help") || args.includes("-h") if ( showHelp || - (!showSystem && !showNudge && !showPruneList && !showSquashContext && !showCooldown) + (!showSystem && !showNudge && !showPruneList && !showCompressContext && !showCooldown) ) { console.log(` -Usage: bun run dcp [TYPE] [-d] [-e] [-s] +Usage: bun run dcp [TYPE] [-p] [-d] [-c] Types: - --system System prompt - --nudge Nudge prompt - --prune-list Example prunable tools list - --squash-context Example squash context - --cooldown Cooldown message after pruning + --system System prompt + --nudge Nudge prompt + --prune-list Example prunable tools list + --compress-context Example compress context + --cooldown Cooldown message after pruning Tool flags (for --system and --nudge): - -d, --discard Enable discard tool - -e, --extract Enable extract tool - -s, --squash Enable squash tool + -p, --prune Enable prune tool + -d, --distill Enable distill tool + -c, --compress Enable compress tool If no tool flags specified, all are enabled. Examples: - bun run dcp --system -d -e -s # System prompt with all tools - bun run dcp --system -d # System prompt with discard only - bun run dcp --nudge -e -s # Nudge with extract and squash + bun run dcp --system -p -d -c # System prompt with all tools + bun run dcp --system -p # System prompt with prune only + bun run dcp --nudge -d -c # Nudge with distill and compress bun run dcp --prune-list # Example prunable tools list `) process.exit(0) @@ -68,9 +68,9 @@ const header = (title: string) => { if (showSystem) { const enabled = [ - flags.discard && "discard", - flags.extract && "extract", - flags.squash && "squash", + flags.prune && "prune", + flags.distill && "distill", + flags.compress && "compress", ] .filter(Boolean) .join(", ") @@ -80,9 +80,9 @@ if (showSystem) { if (showNudge) { const enabled = [ - flags.discard && "discard", - flags.extract && "extract", - flags.squash && "squash", + flags.prune && "prune", + flags.distill && "distill", + flags.compress && "compress", ] .filter(Boolean) .join(", ") @@ -99,16 +99,16 @@ if (showPruneList) { console.log(wrapPrunableTools(mockList)) } -if (showSquashContext) { - header("SQUASH CONTEXT (mock example)") - console.log(wrapSquashContext(45)) +if (showCompressContext) { + header("COMPRESS CONTEXT (mock example)") + console.log(wrapCompressContext(45)) } if (showCooldown) { const enabled = [ - flags.discard && "discard", - flags.extract && "extract", - flags.squash && "squash", + flags.prune && "prune", + flags.distill && "distill", + flags.compress && "compress", ] .filter(Boolean) .join(", ") diff --git a/dcp.schema.json b/dcp.schema.json index bd458ac3..9d87cbac 100644 --- a/dcp.schema.json +++ b/dcp.schema.json @@ -105,27 +105,27 @@ } } }, - "discard": { + "prune": { "type": "object", - "description": "Configuration for the discard tool", + "description": "Configuration for the prune tool", "additionalProperties": false, "properties": { "enabled": { "type": "boolean", "default": true, - "description": "Enable the discard tool" + "description": "Enable the prune tool" } } }, - "extract": { + "distill": { "type": "object", - "description": "Configuration for the extract tool", + "description": "Configuration for the distill tool", "additionalProperties": false, "properties": { "enabled": { "type": "boolean", "default": true, - "description": "Enable the extract tool" + "description": "Enable the distill tool" }, "showDistillation": { "type": "boolean", @@ -134,15 +134,15 @@ } } }, - "squash": { + "compress": { "type": "object", - "description": "Configuration for the squash tool", + "description": "Configuration for the compress tool", "additionalProperties": false, "properties": { "enabled": { "type": "boolean", "default": true, - "description": "Enable the squash tool" + "description": "Enable the compress tool" }, "showSummary": { "type": "boolean", diff --git a/index.ts b/index.ts index fc8ab62a..1537a52b 100644 --- a/index.ts +++ b/index.ts @@ -2,7 +2,7 @@ import type { Plugin } from "@opencode-ai/plugin" import { getConfig } from "./lib/config" import { Logger } from "./lib/logger" import { createSessionState } from "./lib/state" -import { createDiscardTool, createExtractTool, createSquashTool } from "./lib/strategies" +import { createPruneTool, createDistillTool, createCompressTool } from "./lib/strategies" import { createChatMessageTransformHandler, createCommandExecuteHandler, @@ -61,8 +61,8 @@ const plugin: Plugin = (async (ctx) => { ctx.directory, ), tool: { - ...(config.tools.discard.enabled && { - discard: createDiscardTool({ + ...(config.tools.prune.enabled && { + prune: createPruneTool({ client: ctx.client, state, logger, @@ -70,8 +70,8 @@ const plugin: Plugin = (async (ctx) => { workingDirectory: ctx.directory, }), }), - ...(config.tools.extract.enabled && { - extract: createExtractTool({ + ...(config.tools.distill.enabled && { + distill: createDistillTool({ client: ctx.client, state, logger, @@ -79,8 +79,8 @@ const plugin: Plugin = (async (ctx) => { workingDirectory: ctx.directory, }), }), - ...(config.tools.squash.enabled && { - squash: createSquashTool({ + ...(config.tools.compress.enabled && { + compress: createCompressTool({ client: ctx.client, state, logger, @@ -99,9 +99,9 @@ const plugin: Plugin = (async (ctx) => { } const toolsToAdd: string[] = [] - if (config.tools.discard.enabled) toolsToAdd.push("discard") - if (config.tools.extract.enabled) toolsToAdd.push("extract") - if (config.tools.squash.enabled) toolsToAdd.push("squash") + if (config.tools.prune.enabled) toolsToAdd.push("prune") + if (config.tools.distill.enabled) toolsToAdd.push("distill") + if (config.tools.compress.enabled) toolsToAdd.push("compress") if (toolsToAdd.length > 0) { const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? [] diff --git a/lib/config.ts b/lib/config.ts index e0b0b7f8..5f3b4e39 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -9,16 +9,16 @@ export interface Deduplication { protectedTools: string[] } -export interface DiscardTool { +export interface PruneTool { enabled: boolean } -export interface ExtractTool { +export interface DistillTool { enabled: boolean showDistillation: boolean } -export interface SquashTool { +export interface CompressTool { enabled: boolean showSummary: boolean } @@ -31,9 +31,9 @@ export interface ToolSettings { export interface Tools { settings: ToolSettings - discard: DiscardTool - extract: ExtractTool - squash: SquashTool + prune: PruneTool + distill: DistillTool + compress: CompressTool } export interface Commands { @@ -75,9 +75,9 @@ const DEFAULT_PROTECTED_TOOLS = [ "task", "todowrite", "todoread", - "discard", - "extract", - "squash", + "prune", + "distill", + "compress", "batch", "write", "edit", @@ -105,14 +105,14 @@ export const VALID_CONFIG_KEYS = new Set([ "tools.settings.nudgeEnabled", "tools.settings.nudgeFrequency", "tools.settings.protectedTools", - "tools.discard", - "tools.discard.enabled", - "tools.extract", - "tools.extract.enabled", - "tools.extract.showDistillation", - "tools.squash", - "tools.squash.enabled", - "tools.squash.showSummary", + "tools.prune", + "tools.prune.enabled", + "tools.distill", + "tools.distill.enabled", + "tools.distill.showDistillation", + "tools.compress", + "tools.compress.enabled", + "tools.compress.showSummary", "strategies", // strategies.deduplication "strategies.deduplication", @@ -277,50 +277,53 @@ function validateConfigTypes(config: Record): ValidationError[] { }) } } - if (tools.discard) { - if (tools.discard.enabled !== undefined && typeof tools.discard.enabled !== "boolean") { + if (tools.prune) { + if (tools.prune.enabled !== undefined && typeof tools.prune.enabled !== "boolean") { errors.push({ - key: "tools.discard.enabled", + key: "tools.prune.enabled", expected: "boolean", - actual: typeof tools.discard.enabled, + actual: typeof tools.prune.enabled, }) } } - if (tools.extract) { - if (tools.extract.enabled !== undefined && typeof tools.extract.enabled !== "boolean") { + if (tools.distill) { + if (tools.distill.enabled !== undefined && typeof tools.distill.enabled !== "boolean") { errors.push({ - key: "tools.extract.enabled", + key: "tools.distill.enabled", expected: "boolean", - actual: typeof tools.extract.enabled, + actual: typeof tools.distill.enabled, }) } if ( - tools.extract.showDistillation !== undefined && - typeof tools.extract.showDistillation !== "boolean" + tools.distill.showDistillation !== undefined && + typeof tools.distill.showDistillation !== "boolean" ) { errors.push({ - key: "tools.extract.showDistillation", + key: "tools.distill.showDistillation", expected: "boolean", - actual: typeof tools.extract.showDistillation, + actual: typeof tools.distill.showDistillation, }) } } - if (tools.squash) { - if (tools.squash.enabled !== undefined && typeof tools.squash.enabled !== "boolean") { + if (tools.compress) { + if ( + tools.compress.enabled !== undefined && + typeof tools.compress.enabled !== "boolean" + ) { errors.push({ - key: "tools.squash.enabled", + key: "tools.compress.enabled", expected: "boolean", - actual: typeof tools.squash.enabled, + actual: typeof tools.compress.enabled, }) } if ( - tools.squash.showSummary !== undefined && - typeof tools.squash.showSummary !== "boolean" + tools.compress.showSummary !== undefined && + typeof tools.compress.showSummary !== "boolean" ) { errors.push({ - key: "tools.squash.showSummary", + key: "tools.compress.showSummary", expected: "boolean", - actual: typeof tools.squash.showSummary, + actual: typeof tools.compress.showSummary, }) } } @@ -468,14 +471,14 @@ const defaultConfig: PluginConfig = { nudgeFrequency: 10, protectedTools: [...DEFAULT_PROTECTED_TOOLS], }, - discard: { + prune: { enabled: true, }, - extract: { + distill: { enabled: true, showDistillation: false, }, - squash: { + compress: { enabled: true, showSummary: true, }, @@ -644,16 +647,16 @@ function mergeTools( ]), ], }, - discard: { - enabled: override.discard?.enabled ?? base.discard.enabled, + prune: { + enabled: override.prune?.enabled ?? base.prune.enabled, }, - extract: { - enabled: override.extract?.enabled ?? base.extract.enabled, - showDistillation: override.extract?.showDistillation ?? base.extract.showDistillation, + distill: { + enabled: override.distill?.enabled ?? base.distill.enabled, + showDistillation: override.distill?.showDistillation ?? base.distill.showDistillation, }, - squash: { - enabled: override.squash?.enabled ?? base.squash.enabled, - showSummary: override.squash?.showSummary ?? base.squash.showSummary, + compress: { + enabled: override.compress?.enabled ?? base.compress.enabled, + showSummary: override.compress?.showSummary ?? base.compress.showSummary, }, } } @@ -684,9 +687,9 @@ function deepCloneConfig(config: PluginConfig): PluginConfig { ...config.tools.settings, protectedTools: [...config.tools.settings.protectedTools], }, - discard: { ...config.tools.discard }, - extract: { ...config.tools.extract }, - squash: { ...config.tools.squash }, + prune: { ...config.tools.prune }, + distill: { ...config.tools.distill }, + compress: { ...config.tools.compress }, }, strategies: { deduplication: { diff --git a/lib/hooks.ts b/lib/hooks.ts index 126fec34..c10b6f00 100644 --- a/lib/hooks.ts +++ b/lib/hooks.ts @@ -34,12 +34,12 @@ export function createSystemPromptHandler( } const flags = { - discard: config.tools.discard.enabled, - extract: config.tools.extract.enabled, - squash: config.tools.squash.enabled, + prune: config.tools.prune.enabled, + distill: config.tools.distill.enabled, + compress: config.tools.compress.enabled, } - if (!flags.discard && !flags.extract && !flags.squash) { + if (!flags.prune && !flags.distill && !flags.compress) { return } diff --git a/lib/messages/inject.ts b/lib/messages/inject.ts index cc7ec771..5ebe6552 100644 --- a/lib/messages/inject.ts +++ b/lib/messages/inject.ts @@ -20,21 +20,21 @@ The following tools have been invoked and are available for pruning. This list d ${content} ` -export const wrapSquashContext = (messageCount: number): string => ` -Squash available. Conversation: ${messageCount} messages. -Squash collapses completed task sequences or exploration phases into summaries. +export const wrapCompressContext = (messageCount: number): string => ` +Compress available. Conversation: ${messageCount} messages. +Compress collapses completed task sequences or exploration phases into summaries. Uses text boundaries [startString, endString, topic, summary]. -` +` export const wrapCooldownMessage = (flags: { - discard: boolean - extract: boolean - squash: boolean + prune: boolean + distill: boolean + compress: boolean }): string => { const enabledTools: string[] = [] - if (flags.discard) enabledTools.push("discard") - if (flags.extract) enabledTools.push("extract") - if (flags.squash) enabledTools.push("squash") + if (flags.prune) enabledTools.push("prune") + if (flags.distill) enabledTools.push("distill") + if (flags.compress) enabledTools.push("compress") let toolName: string if (enabledTools.length === 0) { @@ -53,12 +53,12 @@ Context management was just performed. Do NOT use the ${toolName} again. A fresh const getNudgeString = (config: PluginConfig): string => { const flags = { - discard: config.tools.discard.enabled, - extract: config.tools.extract.enabled, - squash: config.tools.squash.enabled, + prune: config.tools.prune.enabled, + distill: config.tools.distill.enabled, + compress: config.tools.compress.enabled, } - if (!flags.discard && !flags.extract && !flags.squash) { + if (!flags.prune && !flags.distill && !flags.compress) { return "" } @@ -67,15 +67,15 @@ const getNudgeString = (config: PluginConfig): string => { const getCooldownMessage = (config: PluginConfig): string => { return wrapCooldownMessage({ - discard: config.tools.discard.enabled, - extract: config.tools.extract.enabled, - squash: config.tools.squash.enabled, + prune: config.tools.prune.enabled, + distill: config.tools.distill.enabled, + compress: config.tools.compress.enabled, }) } -const buildSquashContext = (state: SessionState, messages: WithParts[]): string => { +const buildCompressContext = (state: SessionState, messages: WithParts[]): string => { const messageCount = messages.filter((msg) => !isMessageCompacted(state, msg)).length - return wrapSquashContext(messageCount) + return wrapCompressContext(messageCount) } const buildPrunableToolsList = ( @@ -133,23 +133,23 @@ export const insertPruneToolContext = ( logger: Logger, messages: WithParts[], ): void => { - const discardEnabled = config.tools.discard.enabled - const extractEnabled = config.tools.extract.enabled - const squashEnabled = config.tools.squash.enabled + const pruneEnabled = config.tools.prune.enabled + const distillEnabled = config.tools.distill.enabled + const compressEnabled = config.tools.compress.enabled - if (!discardEnabled && !extractEnabled && !squashEnabled) { + if (!pruneEnabled && !distillEnabled && !compressEnabled) { return } - const discardOrExtractEnabled = discardEnabled || extractEnabled + const pruneOrDistillEnabled = pruneEnabled || distillEnabled const contentParts: string[] = [] if (state.lastToolPrune) { logger.debug("Last tool was prune - injecting cooldown message") contentParts.push(getCooldownMessage(config)) } else { - // Inject only when discard or extract is enabled - if (discardOrExtractEnabled) { + // Inject only when prune or distill is enabled + if (pruneOrDistillEnabled) { const prunableToolsList = buildPrunableToolsList(state, config, logger, messages) if (prunableToolsList) { // logger.debug("prunable-tools: \n" + prunableToolsList) @@ -157,11 +157,11 @@ export const insertPruneToolContext = ( } } - // Inject always when squash is enabled (every turn) - if (squashEnabled) { - const squashContext = buildSquashContext(state, messages) - // logger.debug("squash-context: \n" + squashContext) - contentParts.push(squashContext) + // Inject always when compress is enabled (every turn) + if (compressEnabled) { + const compressContext = buildCompressContext(state, messages) + // logger.debug("compress-context: \n" + compressContext) + contentParts.push(compressContext) } // Add nudge if threshold reached diff --git a/lib/messages/prune.ts b/lib/messages/prune.ts index 65e97dd0..8d616270 100644 --- a/lib/messages/prune.ts +++ b/lib/messages/prune.ts @@ -2,7 +2,7 @@ import type { SessionState, WithParts } from "../state" import type { Logger } from "../logger" import type { PluginConfig } from "../config" import { isMessageCompacted, getLastUserMessage } from "../shared-utils" -import { createSyntheticUserMessage, SQUASH_SUMMARY_PREFIX } from "./utils" +import { createSyntheticUserMessage, COMPRESS_SUMMARY_PREFIX } from "./utils" import type { UserMessage } from "@opencode-ai/sdk/v2" const PRUNED_TOOL_OUTPUT_REPLACEMENT = @@ -16,7 +16,7 @@ export const prune = ( config: PluginConfig, messages: WithParts[], ): void => { - filterSquashedRanges(state, logger, messages) + filterCompressedRanges(state, logger, messages) pruneToolOutputs(state, logger, messages) pruneToolInputs(state, logger, messages) pruneToolErrors(state, logger, messages) @@ -107,7 +107,11 @@ const pruneToolErrors = (state: SessionState, logger: Logger, messages: WithPart } } -const filterSquashedRanges = (state: SessionState, logger: Logger, messages: WithParts[]): void => { +const filterCompressedRanges = ( + state: SessionState, + logger: Logger, + messages: WithParts[], +): void => { if (!state.prune.messageIds?.length) { return } @@ -118,7 +122,7 @@ const filterSquashedRanges = (state: SessionState, logger: Logger, messages: Wit const msgId = msg.info.id // Check if there's a summary to inject at this anchor point - const summary = state.squashSummaries?.find((s) => s.anchorMessageId === msgId) + const summary = state.compressSummaries?.find((s) => s.anchorMessageId === msgId) if (summary) { // Find user message for variant and as base for synthetic message const msgIndex = messages.indexOf(msg) @@ -126,17 +130,17 @@ const filterSquashedRanges = (state: SessionState, logger: Logger, messages: Wit if (userMessage) { const userInfo = userMessage.info as UserMessage - const summaryContent = SQUASH_SUMMARY_PREFIX + summary.summary + const summaryContent = COMPRESS_SUMMARY_PREFIX + summary.summary result.push( createSyntheticUserMessage(userMessage, summaryContent, userInfo.variant), ) - logger.info("Injected squash summary", { + logger.info("Injected compress summary", { anchorMessageId: msgId, summaryLength: summary.summary.length, }) } else { - logger.warn("No user message found for squash summary", { + logger.warn("No user message found for compress summary", { anchorMessageId: msgId, }) } diff --git a/lib/messages/utils.ts b/lib/messages/utils.ts index 9d30b03f..a0035727 100644 --- a/lib/messages/utils.ts +++ b/lib/messages/utils.ts @@ -4,7 +4,7 @@ import { isMessageCompacted } from "../shared-utils" import type { SessionState, WithParts } from "../state" import type { UserMessage } from "@opencode-ai/sdk/v2" -export const SQUASH_SUMMARY_PREFIX = "[Squashed conversation block]\n\n" +export const COMPRESS_SUMMARY_PREFIX = "[Compressed conversation block]\n\n" const generateUniqueId = (prefix: string): string => `${prefix}_${ulid()}` diff --git a/lib/prompts/squash-tool-spec.ts b/lib/prompts/compress-tool-spec.ts similarity index 71% rename from lib/prompts/squash-tool-spec.ts rename to lib/prompts/compress-tool-spec.ts index ab3644b7..0a08b08f 100644 --- a/lib/prompts/squash-tool-spec.ts +++ b/lib/prompts/compress-tool-spec.ts @@ -1,8 +1,8 @@ -export const SQUASH_TOOL_SPEC = `Collapses a contiguous range of conversation into a single summary. +export const COMPRESS_TOOL_SPEC = `Collapses a contiguous range of conversation into a single summary. ## When to Use This Tool -Use \`squash\` when you want to condense an entire sequence of work into a brief summary: +Use \`compress\` when you want to condense an entire sequence of work into a brief summary: - **Phase Completion:** You completed a phase (research, tool calls, implementation) and want to collapse the entire sequence into a summary. - **Exploration Done:** You explored multiple files or ran multiple commands and only need a summary of what you learned. @@ -12,19 +12,19 @@ Use \`squash\` when you want to condense an entire sequence of work into a brief ## When NOT to Use This Tool - **If you need specific details:** If you'll need exact code, file contents, or error messages from the range, keep them. -- **For individual tool outputs:** Use \`discard\` or \`extract\` for single tool outputs. Squash targets conversation ranges. +- **For individual tool outputs:** Use \`prune\` or \`distill\` for single tool outputs. Compress targets conversation ranges. - **If it's recent content:** You may still need recent work for the current phase. ## How It Works -1. \`startString\` — A unique text string that marks the start of the range to squash -2. \`endString\` — A unique text string that marks the end of the range to squash -3. \`topic\` — A short label (3-5 words) describing the squashed content +1. \`startString\` — A unique text string that marks the start of the range to compress +2. \`endString\` — A unique text string that marks the end of the range to compress +3. \`topic\` — A short label (3-5 words) describing the compressed content 4. \`summary\` — The replacement text that will be inserted Everything between startString and endString (inclusive) is removed and replaced with your summary. -**Important:** The squash will FAIL if \`startString\` or \`endString\` is not found in the conversation. The squash will also FAIL if either string is found multiple times. Provide a larger string with more surrounding context to uniquely identify the intended match. +**Important:** The compress will FAIL if \`startString\` or \`endString\` is not found in the conversation. The compress will also FAIL if either string is found multiple times. Provide a larger string with more surrounding context to uniquely identify the intended match. ## Best Practices - **Choose unique strings:** Pick text that appears only once in the conversation. @@ -38,10 +38,10 @@ Everything between startString and endString (inclusive) is removed and replaced ## Example - + Conversation: [Asked about auth] -> [Read 5 files] -> [Analyzed patterns] -> [Found "JWT tokens with 24h expiry"] -[Uses squash with: +[Uses compress with: input: [ "Asked about authentication", "JWT tokens with 24h expiry", @@ -49,9 +49,9 @@ Conversation: [Asked about auth] -> [Read 5 files] -> [Analyzed patterns] -> [Fo "Auth: JWT 24h expiry, bcrypt passwords, refresh rotation. Files: auth.ts, tokens.ts, middleware/auth.ts" ] ] - + Assistant: [Just finished reading auth.ts] -I've read the auth file and now need to make edits based on it. I'm keeping this in context rather than squashing. +I've read the auth file and now need to make edits based on it. I'm keeping this in context rather than compressing. ` diff --git a/lib/prompts/discard-tool-spec.ts b/lib/prompts/discard-tool-spec.ts deleted file mode 100644 index 1c1eea74..00000000 --- a/lib/prompts/discard-tool-spec.ts +++ /dev/null @@ -1,39 +0,0 @@ -export const DISCARD_TOOL_SPEC = `Discards tool outputs from context to manage conversation size and reduce noise. - -## IMPORTANT: The Prunable List -A \`\` list is provided to you showing available tool outputs you can discard when there are tools available for pruning. 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 discard. - -## When to Use This Tool - -Use \`discard\` for removing individual tool outputs that are no longer needed: - -- **Noise:** Irrelevant, unhelpful, or superseded outputs that provide no value. -- **Wrong Files:** You read or accessed something that turned out to be irrelevant. -- **Outdated Info:** Outputs that have been superseded by newer information. - -## When NOT to Use This Tool - -- **If the output contains useful information:** Keep it in context rather than discarding. -- **If you'll need the output later:** Don't discard files you plan to edit or context you'll need for implementation. - -## Best Practices -- **Strategic Batching:** Don't discard single small tool outputs (like short bash commands) unless they are pure noise. Wait until you have several items to perform high-impact discards. -- **Think ahead:** Before discarding, ask: "Will I need this output for upcoming work?" If yes, keep it. - -## Format - -- \`ids\`: Array of numeric IDs as strings from the \`\` list - -## Example - - -Assistant: [Reads 'wrong_file.ts'] -This file isn't relevant to the auth system. I'll remove it to clear the context. -[Uses discard with ids: ["5"]] - - - -Assistant: [Reads config.ts, then reads updated config.ts after changes] -The first read is now outdated. I'll discard it and keep the updated version. -[Uses discard with ids: ["20"]] -` diff --git a/lib/prompts/extract-tool-spec.ts b/lib/prompts/distill-tool-spec.ts similarity index 75% rename from lib/prompts/extract-tool-spec.ts rename to lib/prompts/distill-tool-spec.ts index f680ea9e..9fccc048 100644 --- a/lib/prompts/extract-tool-spec.ts +++ b/lib/prompts/distill-tool-spec.ts @@ -1,11 +1,11 @@ -export const EXTRACT_TOOL_SPEC = `Extracts key findings from tool outputs into distilled knowledge, then removes the raw outputs from context. +export const DISTILL_TOOL_SPEC = `Distills key findings from tool outputs into preserved knowledge, then removes the raw outputs from context. ## IMPORTANT: The Prunable List -A \`\` list is provided to you showing available tool outputs you can extract from when there are tools available for pruning. 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 extract. +A \`\` list is provided to you showing available tool outputs you can distill from when there are tools available for pruning. 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 distill. ## When to Use This Tool -Use \`extract\` when you have individual tool outputs with valuable information you want to **preserve in distilled form** before removing the raw content: +Use \`distill\` when you have individual tool outputs with valuable information you want to **preserve in distilled form** before removing the raw content: - **Large Outputs:** The raw output is too large but contains valuable technical details worth keeping. - **Knowledge Preservation:** You have context that contains valuable information (signatures, logic, constraints) but also a lot of unnecessary detail. @@ -17,8 +17,8 @@ Use \`extract\` when you have individual tool outputs with valuable information ## Best Practices -- **Strategic Batching:** Wait until you have several items or a few large outputs to extract, rather than doing tiny, frequent extractions. Aim for high-impact extractions that significantly reduce context size. -- **Think ahead:** Before extracting, ask: "Will I need the raw output for upcoming work?" If you researched a file you'll later edit, do NOT extract it. +- **Strategic Batching:** Wait until you have several items or a few large outputs to distill, rather than doing tiny, frequent distillations. Aim for high-impact distillations that significantly reduce context size. +- **Think ahead:** Before distilling, ask: "Will I need the raw output for upcoming work?" If you researched a file you'll later edit, do NOT distill it. ## Format @@ -29,19 +29,19 @@ Each distillation string should capture the essential information you need to pr ## Example - + Assistant: [Reads auth service and user types] -I'll preserve the key details before extracting. -[Uses extract with: +I'll preserve the key details before distilling. +[Uses distill with: ids: ["10", "11"], distillation: [ "auth.ts: validateToken(token: string) -> User|null checks cache first (5min TTL) then OIDC. hashPassword uses bcrypt 12 rounds. Tokens must be 128+ chars.", "user.ts: interface User { id: string; email: string; permissions: ('read'|'write'|'admin')[]; status: 'active'|'suspended' }" ] ] - + Assistant: [Reads 'auth.ts' to understand the login flow] -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 extracting. +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. ` diff --git a/lib/prompts/index.ts b/lib/prompts/index.ts index a9d70b10..3cfd4295 100644 --- a/lib/prompts/index.ts +++ b/lib/prompts/index.ts @@ -3,9 +3,9 @@ import { dirname, join } from "node:path" import { fileURLToPath } from "node:url" // Tool specs -import { DISCARD_TOOL_SPEC } from "./discard-tool-spec" -import { EXTRACT_TOOL_SPEC } from "./extract-tool-spec" -import { SQUASH_TOOL_SPEC } from "./squash-tool-spec" +import { PRUNE_TOOL_SPEC } from "./prune-tool-spec" +import { DISTILL_TOOL_SPEC } from "./distill-tool-spec" +import { COMPRESS_TOOL_SPEC } from "./compress-tool-spec" const __dirname = dirname(fileURLToPath(import.meta.url)) @@ -14,13 +14,13 @@ const SYSTEM_PROMPT = readFileSync(join(__dirname, "system.md"), "utf-8") const NUDGE = readFileSync(join(__dirname, "nudge.md"), "utf-8") export interface ToolFlags { - discard: boolean - extract: boolean - squash: boolean + prune: boolean + distill: boolean + compress: boolean } function processConditionals(template: string, flags: ToolFlags): string { - const tools = ["discard", "extract", "squash"] as const + const tools = ["prune", "distill", "compress"] as const let result = template // Strip comments: // ... // result = result.replace(/\/\/.*?\/\//g, "") @@ -42,9 +42,9 @@ export function renderNudge(flags: ToolFlags): string { } const PROMPTS: Record = { - "discard-tool-spec": DISCARD_TOOL_SPEC, - "extract-tool-spec": EXTRACT_TOOL_SPEC, - "squash-tool-spec": SQUASH_TOOL_SPEC, + "prune-tool-spec": PRUNE_TOOL_SPEC, + "distill-tool-spec": DISTILL_TOOL_SPEC, + "compress-tool-spec": COMPRESS_TOOL_SPEC, } export function loadPrompt(name: string, vars?: Record): string { diff --git a/lib/prompts/nudge.md b/lib/prompts/nudge.md index 528b858f..078f166e 100644 --- a/lib/prompts/nudge.md +++ b/lib/prompts/nudge.md @@ -6,7 +6,7 @@ PROTOCOL You should prioritize context management, but do not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, you must perform context management. IMMEDIATE ACTION REQUIRED -KNOWLEDGE PRESERVATION: If holding valuable raw data you POTENTIALLY will need in your task, use the `extract` tool. Produce a high-fidelity distillation to preserve insights - be thorough -NOISE REMOVAL: If you read files or ran commands that yielded no value, use the `discard` tool to remove them. If newer tools supersedes older ones, discard the old -PHASE COMPLETION: If a phase is complete, use the `squash` tool to condense the entire sequence into a detailed summary +KNOWLEDGE PRESERVATION: If holding valuable raw data you POTENTIALLY will need in your task, use the `distill` tool. Produce a high-fidelity distillation to preserve insights - be thorough +NOISE REMOVAL: If you read files or ran commands that yielded no value, use the `prune` tool to remove them. If newer tools supersedes older ones, prune the old +PHASE COMPLETION: If a phase is complete, use the `compress` tool to condense the entire sequence into a detailed summary diff --git a/lib/prompts/prune-tool-spec.ts b/lib/prompts/prune-tool-spec.ts new file mode 100644 index 00000000..c2ea3cbb --- /dev/null +++ b/lib/prompts/prune-tool-spec.ts @@ -0,0 +1,39 @@ +export const PRUNE_TOOL_SPEC = `Prunes tool outputs from context to manage conversation size and reduce noise. + +## IMPORTANT: The Prunable List +A \`\` list is provided to you showing available tool outputs you can prune when there are tools available for pruning. 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. + +## When to Use This Tool + +Use \`prune\` for removing individual tool outputs that are no longer needed: + +- **Noise:** Irrelevant, unhelpful, or superseded outputs that provide no value. +- **Wrong Files:** You read or accessed something that turned out to be irrelevant. +- **Outdated Info:** Outputs that have been superseded by newer information. + +## When NOT to Use This Tool + +- **If the output contains useful information:** Keep it in context rather than pruning. +- **If you'll need the output later:** Don't prune files you plan to edit or context you'll need for implementation. + +## Best Practices +- **Strategic Batching:** Don't prune single small tool outputs (like short bash commands) unless they are pure noise. Wait until you have several items to perform high-impact prunes. +- **Think ahead:** Before pruning, ask: "Will I need this output for upcoming work?" If yes, keep it. + +## Format + +- \`ids\`: Array of numeric IDs as strings from the \`\` list + +## Example + + +Assistant: [Reads 'wrong_file.ts'] +This file isn't relevant to the auth system. I'll remove it to clear the context. +[Uses prune with ids: ["5"]] + + + +Assistant: [Reads config.ts, then reads updated config.ts after changes] +The first read is now outdated. I'll prune it and keep the updated version. +[Uses prune with ids: ["20"]] +` diff --git a/lib/prompts/system.md b/lib/prompts/system.md index f2d0e803..1a9233b2 100644 --- a/lib/prompts/system.md +++ b/lib/prompts/system.md @@ -7,9 +7,9 @@ You are operating in a context-constrained environment and must proactively mana 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. AVAILABLE TOOLS -`discard`: Remove individual tool outputs that are noise, irrelevant, or superseded. No preservation of content. -`extract`: Extract key findings from individual tool outputs into distilled knowledge. Use when you need to preserve valuable technical details. -`squash`: Collapse a contiguous range of conversation (completed phases) into a single summary. +`prune`: Remove individual tool outputs that are noise, irrelevant, or superseded. No preservation of content. +`distill`: Distill key findings from individual tool outputs into preserved knowledge. Use when you need to preserve valuable technical details. +`compress`: Collapse a contiguous range of conversation (completed phases) into a single summary. PRUNE METHODICALLY - BATCH YOUR ACTIONS Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by pruning. Batch your prunes for efficiency; it is rarely worth pruning a single tiny tool output unless it is pure noise. Evaluate what SHOULD be pruned before jumping the gun. @@ -21,41 +21,41 @@ You MUST NOT prune when: 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. - -WHEN TO DISCARD + +WHEN TO PRUNE - **Noise Removal:** Outputs that are irrelevant, unhelpful, or superseded by newer info. - **Wrong Files:** You read or accessed something that turned out to be irrelevant to the current work. - **Outdated Info:** Outputs that have been superseded by newer information. -You WILL evaluate discarding when ANY of these are true: +You WILL evaluate pruning when ANY of these are true: You accessed something that turned out to be irrelevant Information has been superseded by newer outputs You are about to start a new phase of work - + - -WHEN TO EXTRACT + +WHEN TO DISTILL **Large Outputs:** The raw output is too large but contains valuable technical details worth keeping. **Knowledge Preservation:** Valuable context you want to preserve but need to reduce size. Your distillation must be comprehensive, capturing technical details (signatures, logic, constraints) such that the raw output is no longer needed. THINK: high signal, complete technical substitute. -You WILL evaluate extracting when ANY of these are true: +You WILL evaluate distilling when ANY of these are true: - You have large tool outputs with valuable technical details - You need to preserve specific information but reduce context size - You are about to start a new phase of work and want to retain key insights - - - WHEN TO SQUASH + + + WHEN TO COMPRESS - **Phase Completion:** When a phase is complete, condense the entire sequence (research, tool calls, implementation) into a summary. - **Exploration Done:** When you've explored multiple files or ran multiple commands and only need a summary of findings. -You WILL evaluate squashing when ANY of these are true: +You WILL evaluate compressing when ANY of these are true: - Phase is complete - You are about to start a new phase of work - Significant conversation has accumulated that can be summarized - + NOTES When in doubt, KEEP IT. diff --git a/lib/state/persistence.ts b/lib/state/persistence.ts index 91111ef7..11e06a93 100644 --- a/lib/state/persistence.ts +++ b/lib/state/persistence.ts @@ -8,13 +8,13 @@ import * as fs from "fs/promises" import { existsSync } from "fs" import { homedir } from "os" import { join } from "path" -import type { SessionState, SessionStats, Prune, SquashSummary } from "./types" +import type { SessionState, SessionStats, Prune, CompressSummary } from "./types" import type { Logger } from "../logger" export interface PersistedSessionState { sessionName?: string prune: Prune - squashSummaries: SquashSummary[] + compressSummaries: CompressSummary[] stats: SessionStats lastUpdated: string } @@ -46,7 +46,7 @@ export async function saveSessionState( const state: PersistedSessionState = { sessionName: sessionName, prune: sessionState.prune, - squashSummaries: sessionState.squashSummaries, + compressSummaries: sessionState.compressSummaries, stats: sessionState.stats, lastUpdated: new Date().toISOString(), } diff --git a/lib/state/state.ts b/lib/state/state.ts index 98a99693..c8e3866d 100644 --- a/lib/state/state.ts +++ b/lib/state/state.ts @@ -51,7 +51,7 @@ export function createSessionState(): SessionState { toolIds: [], messageIds: [], }, - squashSummaries: [], + compressSummaries: [], stats: { pruneTokenCounter: 0, totalPruneTokens: 0, @@ -72,7 +72,7 @@ export function resetSessionState(state: SessionState): void { toolIds: [], messageIds: [], } - state.squashSummaries = [] + state.compressSummaries = [] state.stats = { pruneTokenCounter: 0, totalPruneTokens: 0, @@ -118,7 +118,7 @@ export async function ensureSessionInitialized( toolIds: persisted.prune.toolIds || [], messageIds: persisted.prune.messageIds || [], } - state.squashSummaries = persisted.squashSummaries || [] + state.compressSummaries = persisted.compressSummaries || [] state.stats = { pruneTokenCounter: persisted.stats?.pruneTokenCounter || 0, totalPruneTokens: persisted.stats?.totalPruneTokens || 0, diff --git a/lib/state/tool-cache.ts b/lib/state/tool-cache.ts index 80837519..cf6ccb98 100644 --- a/lib/state/tool-cache.ts +++ b/lib/state/tool-cache.ts @@ -44,14 +44,14 @@ export async function syncToolCache( state.currentTurn - turnCounter < turnProtectionTurns state.lastToolPrune = - (part.tool === "discard" || - part.tool === "extract" || - part.tool === "squash") && + (part.tool === "prune" || + part.tool === "distill" || + part.tool === "compress") && part.state.status === "completed" const allProtectedTools = config.tools.settings.protectedTools - if (part.tool === "discard" || part.tool === "extract" || part.tool === "squash") { + if (part.tool === "prune" || part.tool === "distill" || part.tool === "compress") { state.nudgeCounter = 0 } else if (!allProtectedTools.includes(part.tool) && !isProtectedByTurn) { state.nudgeCounter++ diff --git a/lib/state/types.ts b/lib/state/types.ts index 330f8c89..d84f0ee5 100644 --- a/lib/state/types.ts +++ b/lib/state/types.ts @@ -20,7 +20,7 @@ export interface SessionStats { totalPruneTokens: number } -export interface SquashSummary { +export interface CompressSummary { anchorMessageId: string summary: string } @@ -34,7 +34,7 @@ export interface SessionState { sessionId: string | null isSubAgent: boolean prune: Prune - squashSummaries: SquashSummary[] + compressSummaries: CompressSummary[] stats: SessionStats toolParameters: Map nudgeCounter: number diff --git a/lib/state/utils.ts b/lib/state/utils.ts index be8a08fe..da96afb1 100644 --- a/lib/state/utils.ts +++ b/lib/state/utils.ts @@ -40,7 +40,7 @@ export function resetOnCompaction(state: SessionState): void { state.toolParameters.clear() state.prune.toolIds = [] state.prune.messageIds = [] - state.squashSummaries = [] + state.compressSummaries = [] state.nudgeCounter = 0 state.lastToolPrune = false } diff --git a/lib/strategies/index.ts b/lib/strategies/index.ts index a995254e..e0680e6b 100644 --- a/lib/strategies/index.ts +++ b/lib/strategies/index.ts @@ -1,4 +1,4 @@ export { deduplicate } from "./deduplication" -export { createDiscardTool, createExtractTool, createSquashTool } from "../tools" +export { createPruneTool, createDistillTool, createCompressTool } from "../tools" export { supersedeWrites } from "./supersede-writes" export { purgeErrors } from "./purge-errors" diff --git a/lib/tools/squash.ts b/lib/tools/compress.ts similarity index 76% rename from lib/tools/squash.ts rename to lib/tools/compress.ts index 9ab9425a..f5c30334 100644 --- a/lib/tools/squash.ts +++ b/lib/tools/compress.ts @@ -1,5 +1,5 @@ import { tool } from "@opencode-ai/plugin" -import type { WithParts, SquashSummary } from "../state" +import type { WithParts, CompressSummary } from "../state" import type { PruneToolContext } from "./types" import { ensureSessionInitialized } from "../state" import { saveSessionState } from "../state/persistence" @@ -11,19 +11,19 @@ import { collectToolIdsInRange, collectMessageIdsInRange, } from "./utils" -import { sendSquashNotification } from "../ui/notification" +import { sendCompressNotification } from "../ui/notification" -const SQUASH_TOOL_DESCRIPTION = loadPrompt("squash-tool-spec") +const COMPRESS_TOOL_DESCRIPTION = loadPrompt("compress-tool-spec") -export function createSquashTool(ctx: PruneToolContext): ReturnType { +export function createCompressTool(ctx: PruneToolContext): ReturnType { return tool({ - description: SQUASH_TOOL_DESCRIPTION, + description: COMPRESS_TOOL_DESCRIPTION, args: { input: tool.schema .array(tool.schema.string()) .length(4) .describe( - "[startString, endString, topic, summary] - 4 required strings: (1) startString: unique text from conversation marking range start, (2) endString: unique text marking range end, (3) topic: short 3-5 word label for UI, (4) summary: comprehensive text replacing all squashed content", + "[startString, endString, topic, summary] - 4 required strings: (1) startString: unique text from conversation marking range start, (2) endString: unique text marking range end, (3) topic: short 3-5 word label for UI, (4) summary: comprehensive text replacing all compressed content", ), }, async execute(args, toolCtx) { @@ -32,7 +32,7 @@ export function createSquashTool(ctx: PruneToolContext): ReturnType const [startString, endString, topic, summary] = args.input - logger.info("Squash tool invoked") + logger.info("Compress tool invoked") // logger.info( // JSON.stringify({ // startString: startString?.substring(0, 50) + "...", @@ -53,14 +53,14 @@ export function createSquashTool(ctx: PruneToolContext): ReturnType messages, startString, logger, - state.squashSummaries, + state.compressSummaries, "startString", ) const endResult = findStringInMessages( messages, endString, logger, - state.squashSummaries, + state.compressSummaries, "endString", ) @@ -86,37 +86,37 @@ export function createSquashTool(ctx: PruneToolContext): ReturnType state.prune.messageIds.push(...containedMessageIds) // Remove any existing summaries whose anchors are now inside this range - // This prevents duplicate injections when a larger squash subsumes a smaller one - const removedSummaries = state.squashSummaries.filter((s) => + // This prevents duplicate injections when a larger compress subsumes a smaller one + const removedSummaries = state.compressSummaries.filter((s) => containedMessageIds.includes(s.anchorMessageId), ) if (removedSummaries.length > 0) { - // logger.info("Removing subsumed squash summaries", { + // logger.info("Removing subsumed compress summaries", { // count: removedSummaries.length, // anchorIds: removedSummaries.map((s) => s.anchorMessageId), // }) - state.squashSummaries = state.squashSummaries.filter( + state.compressSummaries = state.compressSummaries.filter( (s) => !containedMessageIds.includes(s.anchorMessageId), ) } - const squashSummary: SquashSummary = { + const compressSummary: CompressSummary = { anchorMessageId: startResult.messageId, summary: summary, } - state.squashSummaries.push(squashSummary) + state.compressSummaries.push(compressSummary) const contentsToTokenize = collectContentInRange( messages, startResult.messageIndex, endResult.messageIndex, ) - const estimatedSquashedTokens = estimateTokensBatch(contentsToTokenize) + const estimatedCompressedTokens = estimateTokensBatch(contentsToTokenize) - state.stats.pruneTokenCounter += estimatedSquashedTokens + state.stats.pruneTokenCounter += estimatedCompressedTokens const currentParams = getCurrentParams(state, messages, logger) - await sendSquashNotification( + await sendCompressNotification( client, logger, ctx.config, @@ -136,20 +136,20 @@ export function createSquashTool(ctx: PruneToolContext): ReturnType state.stats.pruneTokenCounter = 0 state.nudgeCounter = 0 - // logger.info("Squash range created", { + // logger.info("Compress range created", { // startMessageId: startResult.messageId, // endMessageId: endResult.messageId, // toolIdsRemoved: containedToolIds.length, // messagesInRange: containedMessageIds.length, - // estimatedTokens: estimatedSquashedTokens, + // estimatedTokens: estimatedCompressedTokens, // }) saveSessionState(state, logger).catch((err) => logger.error("Failed to persist state", { error: err.message }), ) - const messagesSquashed = endResult.messageIndex - startResult.messageIndex + 1 - return `Squashed ${messagesSquashed} messages (${containedToolIds.length} tool calls) into summary. The content will be replaced with your summary.` + const messagesCompressed = endResult.messageIndex - startResult.messageIndex + 1 + return `Compressed ${messagesCompressed} messages (${containedToolIds.length} tool calls) into summary. The content will be replaced with your summary.` }, }) } diff --git a/lib/tools/extract.ts b/lib/tools/distill.ts similarity index 82% rename from lib/tools/extract.ts rename to lib/tools/distill.ts index 15e5d7c8..ce224e9d 100644 --- a/lib/tools/extract.ts +++ b/lib/tools/distill.ts @@ -4,16 +4,16 @@ import { executePruneOperation } from "./prune-shared" import { PruneReason } from "../ui/notification" import { loadPrompt } from "../prompts" -const EXTRACT_TOOL_DESCRIPTION = loadPrompt("extract-tool-spec") +const DISTILL_TOOL_DESCRIPTION = loadPrompt("distill-tool-spec") -export function createExtractTool(ctx: PruneToolContext): ReturnType { +export function createDistillTool(ctx: PruneToolContext): ReturnType { return tool({ - description: EXTRACT_TOOL_DESCRIPTION, + description: DISTILL_TOOL_DESCRIPTION, args: { ids: tool.schema .array(tool.schema.string()) .min(1) - .describe("Numeric IDs as strings to extract from the list"), + .describe("Numeric IDs as strings to distill from the list"), distillation: tool.schema .array(tool.schema.string()) .min(1) @@ -24,7 +24,7 @@ export function createExtractTool(ctx: PruneToolContext): ReturnType { +export function createPruneTool(ctx: PruneToolContext): ReturnType { return tool({ - description: DISCARD_TOOL_DESCRIPTION, + description: PRUNE_TOOL_DESCRIPTION, args: { ids: tool.schema .array(tool.schema.string()) .min(1) - .describe("Numeric IDs as strings from the list to discard"), + .describe("Numeric IDs as strings from the list to prune"), }, async execute(args, toolCtx) { const numericIds = args.ids const reason = "noise" - return executePruneOperation(ctx, toolCtx, numericIds, reason, "Discard") + return executePruneOperation(ctx, toolCtx, numericIds, reason, "Prune") }, }) } diff --git a/lib/tools/utils.ts b/lib/tools/utils.ts index d5e4e180..8adec13e 100644 --- a/lib/tools/utils.ts +++ b/lib/tools/utils.ts @@ -1,24 +1,24 @@ -import type { WithParts, SquashSummary } from "../state" +import type { WithParts, CompressSummary } from "../state" import type { Logger } from "../logger" /** * Searches messages for a string and returns the message ID where it's found. * Searches in text parts, tool outputs, tool inputs, and other textual content. - * Also searches through existing squash summaries to enable chained squashing. + * Also searches through existing compress summaries to enable chained compression. * Throws an error if the string is not found or found more than once. */ export function findStringInMessages( messages: WithParts[], searchString: string, logger: Logger, - squashSummaries: SquashSummary[] = [], + compressSummaries: CompressSummary[] = [], stringType: "startString" | "endString", ): { messageId: string; messageIndex: number } { const matches: { messageId: string; messageIndex: number }[] = [] - // First, search through existing squash summaries - // This allows referencing text from previous squash operations - for (const summary of squashSummaries) { + // First, search through existing compress summaries + // This allows referencing text from previous compress operations + for (const summary of compressSummaries) { if (summary.summary.includes(searchString)) { const anchorIndex = messages.findIndex((m) => m.info.id === summary.anchorMessageId) if (anchorIndex !== -1) { diff --git a/lib/ui/notification.ts b/lib/ui/notification.ts index 07ccf41d..7bb17f7b 100644 --- a/lib/ui/notification.ts +++ b/lib/ui/notification.ts @@ -26,7 +26,7 @@ function buildMinimalMessage( ): string { const extractedTokens = countDistillationTokens(distillation) const extractedSuffix = - extractedTokens > 0 ? ` (extracted ${formatTokenCount(extractedTokens)})` : "" + extractedTokens > 0 ? ` (distilled ${formatTokenCount(extractedTokens)})` : "" const reasonSuffix = reason && extractedTokens === 0 ? ` — ${PRUNE_REASON_LABELS[reason]}` : "" let message = formatStatsHeader(state.stats.totalPruneTokens, state.stats.pruneTokenCounter) + @@ -51,7 +51,7 @@ function buildDetailedMessage( const pruneTokenCounterStr = `~${formatTokenCount(state.stats.pruneTokenCounter)}` const extractedTokens = countDistillationTokens(distillation) const extractedSuffix = - extractedTokens > 0 ? `, extracted ${formatTokenCount(extractedTokens)}` : "" + extractedTokens > 0 ? `, distilled ${formatTokenCount(extractedTokens)}` : "" const reasonLabel = reason && extractedTokens === 0 ? ` — ${PRUNE_REASON_LABELS[reason]}` : "" message += `\n\n▣ Pruning (${pruneTokenCounterStr}${extractedSuffix})${reasonLabel}` @@ -85,7 +85,7 @@ export async function sendUnifiedNotification( return false } - const showDistillation = config.tools.extract.showDistillation + const showDistillation = config.tools.distill.showDistillation const message = config.pruneNotification === "minimal" @@ -104,7 +104,7 @@ export async function sendUnifiedNotification( return true } -export async function sendSquashNotification( +export async function sendCompressNotification( client: any, logger: Logger, config: PluginConfig, @@ -137,7 +137,7 @@ export async function sendSquashNotification( endResult.messageIndex, 25, ) - message += `\n\n▣ Squashing (${pruneTokenCounterStr}) ${progressBar}` + message += `\n\n▣ Compressing (${pruneTokenCounterStr}) ${progressBar}` message += `\n→ Topic: ${topic}` message += `\n→ Items: ${messageIds.length} messages` if (toolIds.length > 0) { @@ -145,7 +145,7 @@ export async function sendSquashNotification( } else { message += ` condensed` } - if (config.tools.squash.showSummary) { + if (config.tools.compress.showSummary) { message += `\n→ Summary: ${summary}` } } From 5863a1d0c8f55b85df2dd33c6e28d785cd583303 Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Thu, 29 Jan 2026 06:45:45 +0100 Subject: [PATCH 28/36] reorder tool registering --- index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/index.ts b/index.ts index 1537a52b..94291a6a 100644 --- a/index.ts +++ b/index.ts @@ -61,8 +61,8 @@ const plugin: Plugin = (async (ctx) => { ctx.directory, ), tool: { - ...(config.tools.prune.enabled && { - prune: createPruneTool({ + ...(config.tools.distill.enabled && { + distill: createDistillTool({ client: ctx.client, state, logger, @@ -70,8 +70,8 @@ const plugin: Plugin = (async (ctx) => { workingDirectory: ctx.directory, }), }), - ...(config.tools.distill.enabled && { - distill: createDistillTool({ + ...(config.tools.compress.enabled && { + compress: createCompressTool({ client: ctx.client, state, logger, @@ -79,8 +79,8 @@ const plugin: Plugin = (async (ctx) => { workingDirectory: ctx.directory, }), }), - ...(config.tools.compress.enabled && { - compress: createCompressTool({ + ...(config.tools.prune.enabled && { + prune: createPruneTool({ client: ctx.client, state, logger, From efead8f1caac781eb723291bebc8c3428210bab1 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Thu, 29 Jan 2026 22:09:49 -0500 Subject: [PATCH 29/36] fix: generate .ts from .md at build time for bundler compatibility Fixes issue where readFileSync with __dirname fails when bundled by Bun (same issue as #222, reintroduced by #327). - Add scripts/generate-prompts.ts prebuild script - Import generated .ts files instead of runtime readFileSync - Remove postbuild .md copy (no longer needed) --- .gitignore | 3 +++ lib/prompts/index.ts | 12 +++------- package.json | 3 ++- scripts/generate-prompts.ts | 44 +++++++++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 10 deletions(-) create mode 100644 scripts/generate-prompts.ts diff --git a/.gitignore b/.gitignore index 177c17b2..5bf7a25f 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,9 @@ Thumbs.db # OpenCode .opencode/ +# Generated prompt files (from scripts/generate-prompts.ts) +lib/prompts/*.generated.ts + # Tests (local development only) tests/ notes/ diff --git a/lib/prompts/index.ts b/lib/prompts/index.ts index 3cfd4295..44ad6bd8 100644 --- a/lib/prompts/index.ts +++ b/lib/prompts/index.ts @@ -1,17 +1,11 @@ -import { readFileSync } from "node:fs" -import { dirname, join } from "node:path" -import { fileURLToPath } from "node:url" - // Tool specs import { PRUNE_TOOL_SPEC } from "./prune-tool-spec" import { DISTILL_TOOL_SPEC } from "./distill-tool-spec" import { COMPRESS_TOOL_SPEC } from "./compress-tool-spec" -const __dirname = dirname(fileURLToPath(import.meta.url)) - -// Load markdown prompts at module init -const SYSTEM_PROMPT = readFileSync(join(__dirname, "system.md"), "utf-8") -const NUDGE = readFileSync(join(__dirname, "nudge.md"), "utf-8") +// Generated prompts (from .md files via scripts/generate-prompts.ts) +import { SYSTEM as SYSTEM_PROMPT } from "./system.generated" +import { NUDGE } from "./nudge.generated" export interface ToolFlags { prune: boolean diff --git a/package.json b/package.json index 15021281..1587cf9d 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,9 @@ "types": "./dist/index.d.ts", "scripts": { "clean": "rm -rf dist", + "generate:prompts": "tsx scripts/generate-prompts.ts", + "prebuild": "npm run generate:prompts", "build": "npm run clean && tsc", - "postbuild": "rm -rf dist/logs && cp lib/prompts/*.md dist/lib/prompts/", "prepublishOnly": "npm run build", "dev": "opencode plugin dev", "typecheck": "tsc --noEmit", diff --git a/scripts/generate-prompts.ts b/scripts/generate-prompts.ts new file mode 100644 index 00000000..815a54db --- /dev/null +++ b/scripts/generate-prompts.ts @@ -0,0 +1,44 @@ +#!/usr/bin/env tsx +/** + * Prebuild script that generates TypeScript files from Markdown prompts. + * + * This solves the issue where readFileSync with __dirname fails when the + * package is bundled by Bun (see issue #222, PR #272, #327). + * + * The .md files are kept for convenient editing, and this script generates + * .ts files with exported string constants that bundle correctly. + */ + +import { readFileSync, writeFileSync, readdirSync } from "node:fs" +import { dirname, join, basename } from "node:path" +import { fileURLToPath } from "node:url" + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const PROMPTS_DIR = join(__dirname, "..", "lib", "prompts") + +// Find all .md files in the prompts directory +const mdFiles = readdirSync(PROMPTS_DIR).filter((f) => f.endsWith(".md")) + +for (const mdFile of mdFiles) { + const mdPath = join(PROMPTS_DIR, mdFile) + const baseName = basename(mdFile, ".md") + const constName = baseName.toUpperCase().replace(/-/g, "_") + const tsPath = join(PROMPTS_DIR, `${baseName}.generated.ts`) + + const content = readFileSync(mdPath, "utf-8") + + // Escape backticks and ${} template expressions for safe embedding in template literal + const escaped = content.replace(/\\/g, "\\\\").replace(/`/g, "\\`").replace(/\$\{/g, "\\${") + + const tsContent = `// AUTO-GENERATED FILE - DO NOT EDIT +// Generated from ${mdFile} by scripts/generate-prompts.ts +// To modify, edit ${mdFile} and run \`npm run generate:prompts\` + +export const ${constName} = \`${escaped}\` +` + + writeFileSync(tsPath, tsContent) + console.log(`Generated: ${baseName}.generated.ts`) +} + +console.log(`Done! Generated ${mdFiles.length} TypeScript file(s) from Markdown prompts.`) From cdb5862a67bf639b21ccc499420e01c1598186a0 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Thu, 29 Jan 2026 22:12:45 -0500 Subject: [PATCH 30/36] refactor: consolidate cli/ into scripts/ --- package.json | 4 ++-- {cli => scripts}/README.md | 0 {cli => scripts}/print.ts | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename {cli => scripts}/README.md (100%) rename {cli => scripts}/print.ts (100%) diff --git a/package.json b/package.json index 1587cf9d..66dbcb42 100644 --- a/package.json +++ b/package.json @@ -13,11 +13,11 @@ "build": "npm run clean && tsc", "prepublishOnly": "npm run build", "dev": "opencode plugin dev", - "typecheck": "tsc --noEmit", + "typecheck": "npm run generate:prompts && tsc --noEmit", "test": "node --import tsx --test tests/*.test.ts", "format": "prettier --write .", "format:check": "prettier --check .", - "dcp": "tsx cli/print.ts" + "dcp": "tsx scripts/print.ts" }, "keywords": [ "opencode", diff --git a/cli/README.md b/scripts/README.md similarity index 100% rename from cli/README.md rename to scripts/README.md diff --git a/cli/print.ts b/scripts/print.ts similarity index 100% rename from cli/print.ts rename to scripts/print.ts From f123eb376e762ec5848ae5d788894bca990e0ffc Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Thu, 29 Jan 2026 22:45:43 -0500 Subject: [PATCH 31/36] Fix context summary output to handle cases where only tools or only messages are pruned Previously the output would show "0 tools, 3 messages" if only messages were pruned. Now it conditionally shows both parts properly, omitting the 0-count category. --- lib/commands/context.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/commands/context.ts b/lib/commands/context.ts index 7ecd91c8..2706290d 100644 --- a/lib/commands/context.ts +++ b/lib/commands/context.ts @@ -234,10 +234,12 @@ function formatContextMessage(breakdown: TokenBreakdown): string { if (breakdown.prunedTokens > 0) { const withoutPruning = breakdown.total + breakdown.prunedTokens - const messagePrunePart = - breakdown.prunedMessageCount > 0 ? `, ${breakdown.prunedMessageCount} messages` : "" + const pruned = [] + if (breakdown.prunedCount > 0) pruned.push(`${breakdown.prunedCount} tools`) + if (breakdown.prunedMessageCount > 0) + pruned.push(`${breakdown.prunedMessageCount} messages`) lines.push( - ` Pruned: ${breakdown.prunedCount} tools${messagePrunePart} (~${formatTokenCount(breakdown.prunedTokens)})`, + ` Pruned: ${pruned.join(", ")} (~${formatTokenCount(breakdown.prunedTokens)})`, ) lines.push(` Current context: ~${formatTokenCount(breakdown.total)}`) lines.push(` Without DCP: ~${formatTokenCount(withoutPruning)}`) From 2cd85fc81a124b4f0c64483c71d593ede48e368c Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Thu, 29 Jan 2026 23:20:19 -0500 Subject: [PATCH 32/36] revert: show cooldown for errored tools to prevent loop behavior Reverts the change that only showed cooldown on completed tools. Errored tools were getting stuck in loops because they would see prunable context and try to prune again. Now they see the cooldown message instead, which prevents immediate re-pruning. --- lib/state/tool-cache.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/state/tool-cache.ts b/lib/state/tool-cache.ts index cf6ccb98..6b0e595c 100644 --- a/lib/state/tool-cache.ts +++ b/lib/state/tool-cache.ts @@ -44,10 +44,7 @@ export async function syncToolCache( state.currentTurn - turnCounter < turnProtectionTurns state.lastToolPrune = - (part.tool === "prune" || - part.tool === "distill" || - part.tool === "compress") && - part.state.status === "completed" + part.tool === "prune" || part.tool === "distill" || part.tool === "compress" const allProtectedTools = config.tools.settings.protectedTools From 866bdc1029f9825fb53822a4dfc563c26b8feede Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Fri, 30 Jan 2026 00:00:00 -0500 Subject: [PATCH 33/36] spell check --- lib/prompts/system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/prompts/system.md b/lib/prompts/system.md index 1a9233b2..a0574236 100644 --- a/lib/prompts/system.md +++ b/lib/prompts/system.md @@ -62,7 +62,7 @@ When in doubt, KEEP IT. // **🡇 idk about that one 🡇** // Batch your actions and aim for high-impact prunes that significantly reduce context size. FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES. -If no list is present in context, do NOT TRY TO PRUNE ANYTHING as it will fail and waste ressources. +If no list is present in context, do NOT TRY TO PRUNE ANYTHING as it will fail and waste resources. There may be tools in session context that do not appear in the list, this is expected, you can ONLY prune what you see in . From d66a6c20303c9575a2c4506031bf2241193643d8 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Fri, 30 Jan 2026 00:09:02 -0500 Subject: [PATCH 34/36] docs: standardize DCP tool order to distill, compress, prune Update all documentation and source files to consistently order the DCP tools as DCP (Distill, Compress, Prune) rather than various inconsistent orderings. Files updated: - README.md: Tools section and protected tools list - dcp.schema.json: Tool configuration properties - lib/prompts/index.ts: ToolFlags interface and tools array - lib/state/tool-cache.ts: Tool name checks - scripts/README.md: Tool flags table --- README.md | 6 +++--- dcp.schema.json | 24 ++++++++++++------------ lib/prompts/index.ts | 4 ++-- lib/state/tool-cache.ts | 4 ++-- scripts/README.md | 2 +- 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index afd338c3..c60415fa 100644 --- a/README.md +++ b/README.md @@ -28,12 +28,12 @@ DCP uses multiple tools and strategies to reduce context size: ### Tools -**Prune** — Exposes a `prune` tool that the AI can call to remove completed or noisy tool content from context. - **Distill** — Exposes a `distill` tool that the AI can call to distill valuable context into concise summaries before removing the tool content. **Compress** — Exposes a `compress` tool that the AI can call to collapse a large section of conversation (messages and tools) into a single summary. +**Prune** — Exposes a `prune` tool that the AI can call to remove completed or noisy tool content from context. + ### Strategies **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. @@ -157,7 +157,7 @@ When enabled, turn protection prevents tool outputs from being pruned for a conf ### Protected Tools By default, these tools are always protected from pruning across all strategies: -`task`, `todowrite`, `todoread`, `prune`, `distill`, `compress`, `batch`, `write`, `edit`, `plan_enter`, `plan_exit` +`task`, `todowrite`, `todoread`, `distill`, `compress`, `prune`, `batch`, `write`, `edit`, `plan_enter`, `plan_exit` The `protectedTools` arrays in each section add to this default list. diff --git a/dcp.schema.json b/dcp.schema.json index 9d87cbac..70aa5d4b 100644 --- a/dcp.schema.json +++ b/dcp.schema.json @@ -105,18 +105,6 @@ } } }, - "prune": { - "type": "object", - "description": "Configuration for the prune tool", - "additionalProperties": false, - "properties": { - "enabled": { - "type": "boolean", - "default": true, - "description": "Enable the prune tool" - } - } - }, "distill": { "type": "object", "description": "Configuration for the distill tool", @@ -150,6 +138,18 @@ "description": "Show summary output in the UI" } } + }, + "prune": { + "type": "object", + "description": "Configuration for the prune tool", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "description": "Enable the prune tool" + } + } } } }, diff --git a/lib/prompts/index.ts b/lib/prompts/index.ts index 44ad6bd8..c80115f4 100644 --- a/lib/prompts/index.ts +++ b/lib/prompts/index.ts @@ -8,13 +8,13 @@ import { SYSTEM as SYSTEM_PROMPT } from "./system.generated" import { NUDGE } from "./nudge.generated" export interface ToolFlags { - prune: boolean distill: boolean compress: boolean + prune: boolean } function processConditionals(template: string, flags: ToolFlags): string { - const tools = ["prune", "distill", "compress"] as const + const tools = ["distill", "compress", "prune"] as const let result = template // Strip comments: // ... // result = result.replace(/\/\/.*?\/\//g, "") diff --git a/lib/state/tool-cache.ts b/lib/state/tool-cache.ts index 6b0e595c..b5ad154e 100644 --- a/lib/state/tool-cache.ts +++ b/lib/state/tool-cache.ts @@ -44,11 +44,11 @@ export async function syncToolCache( state.currentTurn - turnCounter < turnProtectionTurns state.lastToolPrune = - part.tool === "prune" || part.tool === "distill" || part.tool === "compress" + part.tool === "distill" || part.tool === "compress" || part.tool === "prune" const allProtectedTools = config.tools.settings.protectedTools - if (part.tool === "prune" || part.tool === "distill" || part.tool === "compress") { + if (part.tool === "distill" || part.tool === "compress" || part.tool === "prune") { state.nudgeCounter = 0 } else if (!allProtectedTools.includes(part.tool) && !isProtectedByTurn) { state.nudgeCounter++ diff --git a/scripts/README.md b/scripts/README.md index abbd3398..a99c256b 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -21,9 +21,9 @@ bun run dcp [TYPE] [-p] [-d] [-c] | Flag | Description | | ---------------- | -------------------- | -| `-p, --prune` | Enable prune tool | | `-d, --distill` | Enable distill tool | | `-c, --compress` | Enable compress tool | +| `-p, --prune` | Enable prune tool | If no tool flags specified, all are enabled. From 22f5848cf721b06987b0748d2c4e7659dcbea749 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Fri, 30 Jan 2026 00:27:13 -0500 Subject: [PATCH 35/36] Rename compress tool config from showSummary to showCompression --- README.md | 2 +- dcp.schema.json | 2 +- lib/config.ts | 16 ++++++++-------- lib/ui/notification.ts | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c60415fa..afb761a5 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ DCP uses its own config file: "compress": { "enabled": true, // Show summary content as an ignored message notification - "showSummary": true, + "showCompression": true, }, }, // Automatic pruning strategies diff --git a/dcp.schema.json b/dcp.schema.json index 70aa5d4b..a9bfed22 100644 --- a/dcp.schema.json +++ b/dcp.schema.json @@ -132,7 +132,7 @@ "default": true, "description": "Enable the compress tool" }, - "showSummary": { + "showCompression": { "type": "boolean", "default": true, "description": "Show summary output in the UI" diff --git a/lib/config.ts b/lib/config.ts index 5f3b4e39..337ddea0 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -20,7 +20,7 @@ export interface DistillTool { export interface CompressTool { enabled: boolean - showSummary: boolean + showCompression: boolean } export interface ToolSettings { @@ -112,7 +112,7 @@ export const VALID_CONFIG_KEYS = new Set([ "tools.distill.showDistillation", "tools.compress", "tools.compress.enabled", - "tools.compress.showSummary", + "tools.compress.showCompression", "strategies", // strategies.deduplication "strategies.deduplication", @@ -317,13 +317,13 @@ function validateConfigTypes(config: Record): ValidationError[] { }) } if ( - tools.compress.showSummary !== undefined && - typeof tools.compress.showSummary !== "boolean" + tools.compress.showCompression !== undefined && + typeof tools.compress.showCompression !== "boolean" ) { errors.push({ - key: "tools.compress.showSummary", + key: "tools.compress.showCompression", expected: "boolean", - actual: typeof tools.compress.showSummary, + actual: typeof tools.compress.showCompression, }) } } @@ -480,7 +480,7 @@ const defaultConfig: PluginConfig = { }, compress: { enabled: true, - showSummary: true, + showCompression: true, }, }, strategies: { @@ -656,7 +656,7 @@ function mergeTools( }, compress: { enabled: override.compress?.enabled ?? base.compress.enabled, - showSummary: override.compress?.showSummary ?? base.compress.showSummary, + showCompression: override.compress?.showCompression ?? base.compress.showCompression, }, } } diff --git a/lib/ui/notification.ts b/lib/ui/notification.ts index 7bb17f7b..ec6d399b 100644 --- a/lib/ui/notification.ts +++ b/lib/ui/notification.ts @@ -145,8 +145,8 @@ export async function sendCompressNotification( } else { message += ` condensed` } - if (config.tools.compress.showSummary) { - message += `\n→ Summary: ${summary}` + if (config.tools.compress.showCompression) { + message += `\n→ Compression: ${summary}` } } From 9ad912c35fb2c7d00fdb75f9acd0b092f2bfb7cd Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Fri, 30 Jan 2026 00:32:30 -0500 Subject: [PATCH 36/36] chore: bump version to 1.3.1-beta.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 10ecb124..2efc0095 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tarquinen/opencode-dcp", - "version": "1.3.1-beta.0", + "version": "1.3.1-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tarquinen/opencode-dcp", - "version": "1.3.1-beta.0", + "version": "1.3.1-beta.2", "license": "MIT", "dependencies": { "@anthropic-ai/tokenizer": "^0.0.4", diff --git a/package.json b/package.json index 66dbcb42..b03bd432 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@tarquinen/opencode-dcp", - "version": "1.3.1-beta.0", + "version": "1.3.1-beta.2", "type": "module", "description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context", "main": "./dist/index.js",