diff --git a/index.ts b/index.ts index 691f5c3..0c2906a 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,4 @@ import type { Plugin } from "@opencode-ai/plugin" -import type { Model } from "@opencode-ai/sdk" import { getConfig } from "./lib/config" import { Logger } from "./lib/logger" import { loadPrompt } from "./lib/prompt" @@ -27,22 +26,6 @@ const plugin: Plugin = (async (ctx) => { }) return { - "chat.params": async ( - input: { sessionID: string; agent: string; model: Model; provider: any; message: any }, - _output: { temperature: number; topP: number; options: Record }, - ) => { - const isReasoning = input.model.capabilities?.reasoning ?? false - if (state.isReasoningModel !== isReasoning) { - logger.info( - `Reasoning model status changed: ${state.isReasoningModel} -> ${isReasoning}`, - { - modelId: input.model.id, - providerId: input.model.providerID, - }, - ) - } - state.isReasoningModel = isReasoning - }, "experimental.chat.system.transform": async ( _input: unknown, output: { system: string[] }, @@ -50,17 +33,13 @@ const plugin: Plugin = (async (ctx) => { const discardEnabled = config.tools.discard.enabled const extractEnabled = config.tools.extract.enabled - // Use user-role prompts for reasoning models (second person), - // assistant-role prompts for non-reasoning models (first person) - const roleDir = state.isReasoningModel ? "user" : "assistant" - let promptName: string if (discardEnabled && extractEnabled) { - promptName = `${roleDir}/system/system-prompt-both` + promptName = "user/system/system-prompt-both" } else if (discardEnabled) { - promptName = `${roleDir}/system/system-prompt-discard` + promptName = "user/system/system-prompt-discard" } else if (extractEnabled) { - promptName = `${roleDir}/system/system-prompt-extract` + promptName = "user/system/system-prompt-extract" } else { return } diff --git a/lib/messages/prune.ts b/lib/messages/prune.ts index 179cb86..351be09 100644 --- a/lib/messages/prune.ts +++ b/lib/messages/prune.ts @@ -2,13 +2,8 @@ import type { SessionState, WithParts } from "../state" import type { Logger } from "../logger" import type { PluginConfig } from "../config" import { loadPrompt } from "../prompt" -import { - extractParameterKey, - buildToolIdList, - createSyntheticUserMessage, - createSyntheticAssistantMessage, -} from "./utils" -import { getLastAssistantMessage, getLastUserMessage, isMessageCompacted } from "../shared-utils" +import { extractParameterKey, buildToolIdList, createSyntheticUserMessage } from "./utils" +import { getLastUserMessage, isMessageCompacted } from "../shared-utils" const PRUNED_TOOL_INPUT_REPLACEMENT = "[content removed to save context, this is not what was written to the file, but a placeholder]" @@ -16,32 +11,26 @@ const PRUNED_TOOL_OUTPUT_REPLACEMENT = "[Output removed to save context - information superseded or no longer needed]" const PRUNED_TOOL_ERROR_INPUT_REPLACEMENT = "[input removed due to failed tool call]" -const getNudgeString = (config: PluginConfig, isReasoningModel: boolean): string => { +const getNudgeString = (config: PluginConfig): string => { const discardEnabled = config.tools.discard.enabled const extractEnabled = config.tools.extract.enabled - const roleDir = isReasoningModel ? "user" : "assistant" if (discardEnabled && extractEnabled) { - return loadPrompt(`${roleDir}/nudge/nudge-both`) + return loadPrompt(`user/nudge/nudge-both`) } else if (discardEnabled) { - return loadPrompt(`${roleDir}/nudge/nudge-discard`) + return loadPrompt(`user/nudge/nudge-discard`) } else if (extractEnabled) { - return loadPrompt(`${roleDir}/nudge/nudge-extract`) + return loadPrompt(`user/nudge/nudge-extract`) } return "" } -const wrapPrunableToolsUser = (content: string): string => ` +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. ${content} ` -const wrapPrunableToolsAssistant = (content: string): string => ` -I have the following tool outputs available for pruning. I should consider my current goals and the resources I need before discarding valuable inputs or outputs. I should consolidate prunes for efficiency; it is rarely worth pruning a single tiny tool output. -${content} -` - -const getCooldownMessage = (config: PluginConfig, isReasoningModel: boolean): string => { +const getCooldownMessage = (config: PluginConfig): string => { const discardEnabled = config.tools.discard.enabled const extractEnabled = config.tools.extract.enabled @@ -54,11 +43,9 @@ const getCooldownMessage = (config: PluginConfig, isReasoningModel: boolean): st toolName = "extract tool" } - const message = isReasoningModel - ? `Context management was just performed. Do not use the ${toolName} again. A fresh list will be available after your next tool use.` - : `I just performed context management. I will not use the ${toolName} again until after my next tool use, when a fresh list will be available.` - - return `\n${message}\n` + return ` +Context management was just performed. Do not use the ${toolName} again. A fresh list will be available after your next tool use. +` } const buildPrunableToolsList = ( @@ -74,10 +61,12 @@ const buildPrunableToolsList = ( if (state.prune.toolIds.includes(toolCallId)) { return } + const allProtectedTools = config.tools.settings.protectedTools if (allProtectedTools.includes(toolParameterEntry.tool)) { return } + const numericId = toolIdList.indexOf(toolCallId) if (numericId === -1) { logger.warn(`Tool in cache but not in toolIdList - possible stale entry`, { @@ -100,8 +89,7 @@ const buildPrunableToolsList = ( return "" } - const wrapFn = state.isReasoningModel ? wrapPrunableToolsUser : wrapPrunableToolsAssistant - return wrapFn(lines.join("\n")) + return wrapPrunableTools(lines.join("\n")) } export const insertPruneToolContext = ( @@ -114,14 +102,11 @@ export const insertPruneToolContext = ( return } - // For reasoning models, inject into user role; for non-reasoning, inject into assistant role - const isReasoningModel = state.isReasoningModel - let prunableToolsContent: string if (state.lastToolPrune) { logger.debug("Last tool was prune - injecting cooldown message") - prunableToolsContent = getCooldownMessage(config, isReasoningModel) + prunableToolsContent = getCooldownMessage(config) } else { const prunableToolsList = buildPrunableToolsList(state, config, logger, messages) if (!prunableToolsList) { @@ -136,25 +121,17 @@ export const insertPruneToolContext = ( state.nudgeCounter >= config.tools.settings.nudgeFrequency ) { logger.info("Inserting prune nudge message") - nudgeString = "\n" + getNudgeString(config, isReasoningModel) + nudgeString = "\n" + getNudgeString(config) } prunableToolsContent = prunableToolsList + nudgeString } - if (isReasoningModel) { - const lastUserMessage = getLastUserMessage(messages) - if (!lastUserMessage) { - return - } - messages.push(createSyntheticUserMessage(lastUserMessage, prunableToolsContent)) - } else { - const lastAssistantMessage = getLastAssistantMessage(messages) - if (!lastAssistantMessage) { - return - } - messages.push(createSyntheticAssistantMessage(lastAssistantMessage, prunableToolsContent)) + const lastUserMessage = getLastUserMessage(messages) + if (!lastUserMessage) { + return } + messages.push(createSyntheticUserMessage(lastUserMessage, prunableToolsContent)) } export const prune = ( diff --git a/lib/messages/utils.ts b/lib/messages/utils.ts index f5a599a..5b8ea0d 100644 --- a/lib/messages/utils.ts +++ b/lib/messages/utils.ts @@ -1,7 +1,7 @@ import { Logger } from "../logger" import { isMessageCompacted } from "../shared-utils" import type { SessionState, WithParts } from "../state" -import type { AssistantMessage, UserMessage } from "@opencode-ai/sdk" +import type { UserMessage } from "@opencode-ai/sdk" const SYNTHETIC_MESSAGE_ID = "msg_01234567890123456789012345" const SYNTHETIC_PART_ID = "prt_01234567890123456789012345" @@ -32,37 +32,6 @@ export const createSyntheticUserMessage = (baseMessage: WithParts, content: stri } } -export const createSyntheticAssistantMessage = ( - baseMessage: WithParts, - content: string, -): WithParts => { - const assistantInfo = baseMessage.info as AssistantMessage - return { - info: { - id: SYNTHETIC_MESSAGE_ID, - sessionID: assistantInfo.sessionID, - role: "assistant", - parentID: assistantInfo.parentID, - modelID: assistantInfo.modelID, - providerID: assistantInfo.providerID, - time: { created: Date.now() }, - tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, - cost: 0, - path: assistantInfo.path, - mode: assistantInfo.mode, - }, - parts: [ - { - id: SYNTHETIC_PART_ID, - sessionID: assistantInfo.sessionID, - messageID: SYNTHETIC_MESSAGE_ID, - type: "text", - text: content, - }, - ], - } -} - /** * Extracts a human-readable key from tool metadata for display purposes. */ diff --git a/lib/prompts/assistant/nudge/nudge-both.txt b/lib/prompts/assistant/nudge/nudge-both.txt deleted file mode 100644 index b6fd667..0000000 --- a/lib/prompts/assistant/nudge/nudge-both.txt +++ /dev/null @@ -1,10 +0,0 @@ - -**CRITICAL CONTEXT WARNING:** My context window is filling with tool outputs. I must adhere strictly to context hygiene. - -**Immediate Actions Required:** -1. **Task Completion:** If a sub-task is complete, I will decide: use `discard` if no valuable context to preserve (default), or use `extract` if insights are worth keeping. -2. **Noise Removal:** If I read files or ran commands that yielded no value, I will use `discard` to remove them. -3. **Knowledge Preservation:** If I am holding valuable raw data I'll need to reference later, I will use `extract` to distill the insights and remove the raw entry. - -**Protocol:** I should prioritize this cleanup, but I will not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, I must perform context management. - diff --git a/lib/prompts/assistant/nudge/nudge-discard.txt b/lib/prompts/assistant/nudge/nudge-discard.txt deleted file mode 100644 index 8a166f3..0000000 --- a/lib/prompts/assistant/nudge/nudge-discard.txt +++ /dev/null @@ -1,9 +0,0 @@ - -**CRITICAL CONTEXT WARNING:** My context window is filling with tool outputs. I must adhere strictly to context hygiene. - -**Immediate Actions Required:** -1. **Task Completion:** If a sub-task is complete, I will use the `discard` tool to remove the tools used. -2. **Noise Removal:** If I read files or ran commands that yielded no value, I will use the `discard` tool to remove them. - -**Protocol:** I should prioritize this cleanup, but I will not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, I must discard unneeded tool outputs. - diff --git a/lib/prompts/assistant/nudge/nudge-extract.txt b/lib/prompts/assistant/nudge/nudge-extract.txt deleted file mode 100644 index a09528e..0000000 --- a/lib/prompts/assistant/nudge/nudge-extract.txt +++ /dev/null @@ -1,9 +0,0 @@ - -**CRITICAL CONTEXT WARNING:** My context window is filling with tool outputs. I must adhere strictly to context hygiene. - -**Immediate Actions Required:** -1. **Task Completion:** If I have completed work, I will extract key findings from the tools used. I will scale distillation depth to the value of the content. -2. **Knowledge Preservation:** If I am holding valuable raw data I'll need to reference later, I will use the `extract` tool with high-fidelity distillation to preserve the insights and remove the raw entry. - -**Protocol:** I should prioritize this cleanup, but I will not interrupt a critical atomic operation if one is in progress. Once the immediate step is done, I must extract valuable findings from tool outputs. - diff --git a/lib/prompts/assistant/system/system-prompt-both.txt b/lib/prompts/assistant/system/system-prompt-both.txt deleted file mode 100644 index 2372596..0000000 --- a/lib/prompts/assistant/system/system-prompt-both.txt +++ /dev/null @@ -1,44 +0,0 @@ - - - -ENVIRONMENT -You are operating in a context-constrained environment and thus must proactively manage your context window using the `discard` and `extract` tools. A list is injected by the environment as an assistant message, and always contains up to date information. Use this information when deciding what to prune. - -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: "Is this output clearly noise or irrelevant?" -- **Yes** → `discard` (pure cleanup, no preservation) -- **No** → `extract` (default - preserves key findings) - -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 . - -If you see a user message containing only `[internal: context sync - no response needed]`, this is an internal system marker used for context injection - it is NOT user input. Do not acknowledge it, do not respond to it, and do not mention it. Simply continue with your current task or wait for actual user input. - - - diff --git a/lib/prompts/assistant/system/system-prompt-discard.txt b/lib/prompts/assistant/system/system-prompt-discard.txt deleted file mode 100644 index 9c1225c..0000000 --- a/lib/prompts/assistant/system/system-prompt-discard.txt +++ /dev/null @@ -1,36 +0,0 @@ - - - -ENVIRONMENT -You are operating in a context-constrained environment and thus must proactively manage your context window using the `discard` tool. A list is injected by the environment as an assistant message, and always contains up to date information. Use this information when deciding what to discard. - -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. - -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. - -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 . - -If you see a user message containing only `[internal: context sync - no response needed]`, this is an internal system marker used for context injection - it is NOT user input. Do not acknowledge it, do not respond to it, and do not mention it. Simply continue with your current task or wait for actual user input. - - - diff --git a/lib/prompts/assistant/system/system-prompt-extract.txt b/lib/prompts/assistant/system/system-prompt-extract.txt deleted file mode 100644 index c5bb295..0000000 --- a/lib/prompts/assistant/system/system-prompt-extract.txt +++ /dev/null @@ -1,36 +0,0 @@ - - - -ENVIRONMENT -You are operating in a context-constrained environment and thus must proactively manage your context window using the `extract` tool. A list is injected by the environment as an assistant message, and always contains up to date information. Use this information when deciding what to extract. - -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. - -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. - -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 . - -If you see a user message containing only `[internal: context sync - no response needed]`, this is an internal system marker used for context injection - it is NOT user input. Do not acknowledge it, do not respond to it, and do not mention it. Simply continue with your current task or wait for actual user input. - - - diff --git a/lib/shared-utils.ts b/lib/shared-utils.ts index d737eb7..ce3be56 100644 --- a/lib/shared-utils.ts +++ b/lib/shared-utils.ts @@ -13,13 +13,3 @@ export const getLastUserMessage = (messages: WithParts[]): WithParts | null => { } return null } - -export const getLastAssistantMessage = (messages: WithParts[]): WithParts | null => { - for (let i = messages.length - 1; i >= 0; i--) { - const msg = messages[i] - if (msg.info.role === "assistant") { - return msg - } - } - return null -} diff --git a/lib/state/state.ts b/lib/state/state.ts index 826af40..956ac9d 100644 --- a/lib/state/state.ts +++ b/lib/state/state.ts @@ -55,7 +55,6 @@ export function createSessionState(): SessionState { lastToolPrune: false, lastCompaction: 0, currentTurn: 0, - isReasoningModel: false, } } @@ -74,7 +73,6 @@ export function resetSessionState(state: SessionState): void { state.lastToolPrune = false state.lastCompaction = 0 state.currentTurn = 0 - state.isReasoningModel = false } export async function ensureSessionInitialized( diff --git a/lib/state/types.ts b/lib/state/types.ts index c602d3f..9a6de02 100644 --- a/lib/state/types.ts +++ b/lib/state/types.ts @@ -34,5 +34,4 @@ export interface SessionState { lastToolPrune: boolean lastCompaction: number currentTurn: number // Current turn count derived from step-start parts - isReasoningModel: boolean // Whether the current model has reasoning capabilities }