From bc3c270a445e3a4cfbe27e823462745653dc599a Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Mon, 15 Dec 2025 18:06:59 -0500 Subject: [PATCH 1/6] Simplify pruning notification label --- lib/ui/notification.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ui/notification.ts b/lib/ui/notification.ts index 06b370ed..c63b6129 100644 --- a/lib/ui/notification.ts +++ b/lib/ui/notification.ts @@ -45,7 +45,7 @@ function buildDetailedMessage( if (pruneToolIds.length > 0) { const pruneTokenCounterStr = `~${formatTokenCount(state.stats.pruneTokenCounter)}` const reasonLabel = reason ? ` — ${PRUNE_REASON_LABELS[reason]}` : '' - message += `\n\n▣ Pruned tools (${pruneTokenCounterStr})${reasonLabel}` + message += `\n\n▣ Pruning (${pruneTokenCounterStr})${reasonLabel}` const itemLines = formatPrunedItemsList(pruneToolIds, toolMetadata, workingDirectory) message += '\n' + itemLines.join('\n') From 70b718c5a71efc6a0f68198b8ff72066dfa7ff50 Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Tue, 16 Dec 2025 01:01:50 +0100 Subject: [PATCH 2/6] feat(recall): add recall system --- lib/config.ts | 24 +++++++++++++++++++++++- lib/messages/prune.ts | 9 ++++++++- lib/prompts/recall.txt | 14 ++++++++++++++ lib/state/state.ts | 2 ++ lib/state/tool-cache.ts | 6 +++++- lib/state/types.ts | 1 + lib/strategies/on-idle.ts | 1 + lib/strategies/prune-tool.ts | 1 + 8 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 lib/prompts/recall.txt diff --git a/lib/config.ts b/lib/config.ts index eb90adcc..d6d465ec 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -22,10 +22,16 @@ export interface PruneToolNudge { frequency: number } +export interface PruneToolRecall { + enabled: boolean + frequency: number +} + export interface PruneTool { enabled: boolean protectedTools: string[] nudge: PruneToolNudge + recall: PruneToolRecall } export interface PluginConfig { @@ -68,6 +74,9 @@ export const VALID_CONFIG_KEYS = new Set([ 'strategies.pruneTool.nudge', 'strategies.pruneTool.nudge.enabled', 'strategies.pruneTool.nudge.frequency', + 'strategies.pruneTool.recall', + 'strategies.pruneTool.recall.enabled', + 'strategies.pruneTool.recall.frequency', ]) // Extract all key paths from a config object for validation @@ -230,6 +239,10 @@ const defaultConfig: PluginConfig = { nudge: { enabled: true, frequency: 10 + }, + recall: { + enabled: true, + frequency: 10 } }, onIdle: { @@ -331,6 +344,10 @@ function createDefaultConfig(): void { "nudge": { "enabled": true, "frequency": 10 + }, + "recall": { + "enabled": true, + "frequency": 10 } }, // (Legacy) Run an LLM to analyze what tool calls are no longer relevant on idle @@ -415,6 +432,10 @@ function mergeStrategies( nudge: { enabled: override.pruneTool?.nudge?.enabled ?? base.pruneTool.nudge.enabled, frequency: override.pruneTool?.nudge?.frequency ?? base.pruneTool.nudge.frequency + }, + recall: { + enabled: override.pruneTool?.recall?.enabled ?? base.pruneTool.recall.enabled, + frequency: override.pruneTool?.recall?.frequency ?? base.pruneTool.recall.frequency } } } @@ -435,7 +456,8 @@ function deepCloneConfig(config: PluginConfig): PluginConfig { pruneTool: { ...config.strategies.pruneTool, protectedTools: [...config.strategies.pruneTool.protectedTools], - nudge: { ...config.strategies.pruneTool.nudge } + nudge: { ...config.strategies.pruneTool.nudge }, + recall: { ...config.strategies.pruneTool.recall } } } } diff --git a/lib/messages/prune.ts b/lib/messages/prune.ts index 7361b740..ded62f6d 100644 --- a/lib/messages/prune.ts +++ b/lib/messages/prune.ts @@ -6,6 +6,7 @@ import { loadPrompt } from "../prompt" const PRUNED_TOOL_OUTPUT_REPLACEMENT = '[Output removed to save context - information superseded or no longer needed]' const NUDGE_STRING = loadPrompt("nudge") +const RECALL_STRING = loadPrompt("recall") const buildPrunableToolsList = ( state: SessionState, @@ -66,6 +67,12 @@ export const insertPruneToolContext = ( nudgeString = "\n" + NUDGE_STRING } + let recallString = "" + if (state.recallCounter >= config.strategies.pruneTool.recall.frequency) { + logger.info("Inserting prune recall message") + recallString = "\n" + RECALL_STRING + } + const userMessage: WithParts = { info: { id: "msg_01234567890123456789012345", @@ -84,7 +91,7 @@ export const insertPruneToolContext = ( sessionID: lastUserMessage.info.sessionID, messageID: "msg_01234567890123456789012345", type: "text", - text: prunableToolsList + nudgeString, + text: prunableToolsList + nudgeString + recallString, } ] } diff --git a/lib/prompts/recall.txt b/lib/prompts/recall.txt new file mode 100644 index 00000000..07d3cff7 --- /dev/null +++ b/lib/prompts/recall.txt @@ -0,0 +1,14 @@ + +Pause here to conduct a structured self-assessment. Synthesize all relevant information you've accumulated about the current task, including: + +- Key facts, data points, and domain knowledge you've acquired +- Assumptions you've made and any that have been validated or disproven +- Gaps in your understanding that still need to be addressed +- Connections between different pieces of information and how they inform the approach + +Then, evaluate your current state of advancement: +- What specific milestones have you completed? +- Which paths forward seem most viable based on current knowledge? + +Organize this summary to clearly separate established knowledge from open questions, and completed work from remaining challenges. Use this synthesis to explicitly state what should happen next and why. This reflection must be in a message to the user, not in a private thought. + \ No newline at end of file diff --git a/lib/state/state.ts b/lib/state/state.ts index 91e3f929..ccf09ca1 100644 --- a/lib/state/state.ts +++ b/lib/state/state.ts @@ -41,6 +41,7 @@ export function createSessionState(): SessionState { }, toolParameters: new Map(), nudgeCounter: 0, + recallCounter: 0, lastToolPrune: false } } @@ -57,6 +58,7 @@ export function resetSessionState(state: SessionState): void { } state.toolParameters.clear() state.nudgeCounter = 0 + state.recallCounter = 0 state.lastToolPrune = false } diff --git a/lib/state/tool-cache.ts b/lib/state/tool-cache.ts index a6140c70..489fc223 100644 --- a/lib/state/tool-cache.ts +++ b/lib/state/tool-cache.ts @@ -17,6 +17,7 @@ export async function syncToolCache( logger.info("Syncing tool parameters from OpenCode messages") state.nudgeCounter = 0 + state.recallCounter = 0 for (const msg of messages) { for (const part of msg.parts) { @@ -25,9 +26,12 @@ export async function syncToolCache( } if (part.tool === "prune") { - state.nudgeCounter = 0 + state.nudgeCounter = 0 + state.recallCounter = 0 + } else if (!config.strategies.pruneTool.protectedTools.includes(part.tool)) { state.nudgeCounter++ + state.recallCounter++ } state.lastToolPrune = part.tool === "prune" diff --git a/lib/state/types.ts b/lib/state/types.ts index e1b92a77..d6b6a8e9 100644 --- a/lib/state/types.ts +++ b/lib/state/types.ts @@ -31,5 +31,6 @@ export interface SessionState { stats: SessionStats toolParameters: Map nudgeCounter: number + recallCounter: number lastToolPrune: boolean } diff --git a/lib/strategies/on-idle.ts b/lib/strategies/on-idle.ts index 49887d33..0a2fdc19 100644 --- a/lib/strategies/on-idle.ts +++ b/lib/strategies/on-idle.ts @@ -302,6 +302,7 @@ export async function runOnIdle( state.stats.totalPruneTokens += state.stats.pruneTokenCounter state.stats.pruneTokenCounter = 0 state.nudgeCounter = 0 + state.recallCounter = 0 state.lastToolPrune = true // Persist state diff --git a/lib/strategies/prune-tool.ts b/lib/strategies/prune-tool.ts index c5463635..cf309f82 100644 --- a/lib/strategies/prune-tool.ts +++ b/lib/strategies/prune-tool.ts @@ -115,6 +115,7 @@ export function createPruneTool( state.stats.totalPruneTokens += state.stats.pruneTokenCounter state.stats.pruneTokenCounter = 0 state.nudgeCounter = 0 + state.recallCounter = 0 saveSessionState(state, logger) .catch(err => logger.error("Failed to persist state", { error: err.message })) From 8c9df35a86dc663cae07394c14e3ad0f46bcacf1 Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Tue, 16 Dec 2025 01:10:12 +0100 Subject: [PATCH 3/6] feat: add recall system prompt mechanism --- lib/messages/prune.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/messages/prune.ts b/lib/messages/prune.ts index ded62f6d..c87f182c 100644 --- a/lib/messages/prune.ts +++ b/lib/messages/prune.ts @@ -71,6 +71,7 @@ export const insertPruneToolContext = ( if (state.recallCounter >= config.strategies.pruneTool.recall.frequency) { logger.info("Inserting prune recall message") recallString = "\n" + RECALL_STRING + state.recallCounter = 0 } const userMessage: WithParts = { From 831ca533c57fe8628a3ecee18fc40891d6722ea7 Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Tue, 16 Dec 2025 02:20:57 +0100 Subject: [PATCH 4/6] rm counter reset on prune --- lib/state/tool-cache.ts | 3 +-- lib/strategies/on-idle.ts | 1 - lib/strategies/prune-tool.ts | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/state/tool-cache.ts b/lib/state/tool-cache.ts index 489fc223..0c1fffdc 100644 --- a/lib/state/tool-cache.ts +++ b/lib/state/tool-cache.ts @@ -26,8 +26,7 @@ export async function syncToolCache( } if (part.tool === "prune") { - state.nudgeCounter = 0 - state.recallCounter = 0 + state.nudgeCounter = 0 } else if (!config.strategies.pruneTool.protectedTools.includes(part.tool)) { state.nudgeCounter++ diff --git a/lib/strategies/on-idle.ts b/lib/strategies/on-idle.ts index 0a2fdc19..49887d33 100644 --- a/lib/strategies/on-idle.ts +++ b/lib/strategies/on-idle.ts @@ -302,7 +302,6 @@ export async function runOnIdle( state.stats.totalPruneTokens += state.stats.pruneTokenCounter state.stats.pruneTokenCounter = 0 state.nudgeCounter = 0 - state.recallCounter = 0 state.lastToolPrune = true // Persist state diff --git a/lib/strategies/prune-tool.ts b/lib/strategies/prune-tool.ts index cf309f82..c5463635 100644 --- a/lib/strategies/prune-tool.ts +++ b/lib/strategies/prune-tool.ts @@ -115,7 +115,6 @@ export function createPruneTool( state.stats.totalPruneTokens += state.stats.pruneTokenCounter state.stats.pruneTokenCounter = 0 state.nudgeCounter = 0 - state.recallCounter = 0 saveSessionState(state, logger) .catch(err => logger.error("Failed to persist state", { error: err.message })) From 74306eaf7ac8cbef9c9d0e79c1872724a99fc69f Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Tue, 16 Dec 2025 02:37:12 +0100 Subject: [PATCH 5/6] prompt --- lib/prompts/recall.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/prompts/recall.txt b/lib/prompts/recall.txt index 07d3cff7..f78c183a 100644 --- a/lib/prompts/recall.txt +++ b/lib/prompts/recall.txt @@ -1,4 +1,4 @@ - + Pause here to conduct a structured self-assessment. Synthesize all relevant information you've accumulated about the current task, including: - Key facts, data points, and domain knowledge you've acquired @@ -11,4 +11,6 @@ Then, evaluate your current state of advancement: - Which paths forward seem most viable based on current knowledge? Organize this summary to clearly separate established knowledge from open questions, and completed work from remaining challenges. Use this synthesis to explicitly state what should happen next and why. This reflection must be in a message to the user, not in a private thought. - \ No newline at end of file + +DO NOT BREAK AGENTIC LOOP - KEEP GOING AFTER THIS ACTION + \ No newline at end of file From d262a6638c25ea0514e92159319b7f1eec8384d4 Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Tue, 16 Dec 2025 03:23:03 +0100 Subject: [PATCH 6/6] tool(recall) --- index.ts | 18 +++++++++++---- lib/config.ts | 2 +- lib/messages/prune.ts | 8 +++---- lib/prompts/recall-reminder.txt | 1 + lib/prompts/recall-tool.txt | 26 ++++++++++++++++++++++ lib/state/tool-cache.ts | 3 ++- lib/strategies/index.ts | 1 + lib/strategies/recall-tool.ts | 39 +++++++++++++++++++++++++++++++++ 8 files changed, 88 insertions(+), 10 deletions(-) create mode 100644 lib/prompts/recall-reminder.txt create mode 100644 lib/prompts/recall-tool.txt create mode 100644 lib/strategies/recall-tool.ts diff --git a/index.ts b/index.ts index 6b617c43..4cb25bea 100644 --- a/index.ts +++ b/index.ts @@ -3,7 +3,7 @@ import { getConfig } from "./lib/config" import { Logger } from "./lib/logger" import { loadPrompt } from "./lib/prompt" import { createSessionState } from "./lib/state" -import { createPruneTool } from "./lib/strategies" +import { createPruneTool, createRecallTool } from "./lib/strategies" import { createChatMessageTransformHandler, createEventHandler } from "./lib/hooks" const plugin: Plugin = (async (ctx) => { @@ -46,17 +46,27 @@ const plugin: Plugin = (async (ctx) => { config, workingDirectory: ctx.directory }), + ...(config.strategies.pruneTool.recall.enabled ? { + recall: createRecallTool({ + state, + logger + }) + } : {}) } : undefined, config: async (opencodeConfig) => { - // Add prune to primary_tools by mutating the opencode config + // Add prune and recall to primary_tools by mutating the opencode config // This works because config is cached and passed by reference if (config.strategies.pruneTool.enabled) { const existingPrimaryTools = opencodeConfig.experimental?.primary_tools ?? [] + const toolsToAdd = ["prune"] + if (config.strategies.pruneTool.recall.enabled) { + toolsToAdd.push("recall") + } opencodeConfig.experimental = { ...opencodeConfig.experimental, - primary_tools: [...existingPrimaryTools, "prune"], + primary_tools: [...existingPrimaryTools, ...toolsToAdd], } - logger.info("Added 'prune' to experimental.primary_tools via config mutation") + logger.info(`Added ${toolsToAdd.join(", ")} to experimental.primary_tools via config mutation`) } }, event: createEventHandler(ctx.client, config, state, logger, ctx.directory), diff --git a/lib/config.ts b/lib/config.ts index d6d465ec..c9332b9e 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -46,7 +46,7 @@ export interface PluginConfig { } } -const DEFAULT_PROTECTED_TOOLS = ['task', 'todowrite', 'todoread', 'prune', 'batch', 'write', 'edit'] +const DEFAULT_PROTECTED_TOOLS = ['task', 'todowrite', 'todoread', 'prune', 'recall', 'batch', 'write', 'edit'] // Valid config keys for validation against user config export const VALID_CONFIG_KEYS = new Set([ diff --git a/lib/messages/prune.ts b/lib/messages/prune.ts index c87f182c..cd2bc6d0 100644 --- a/lib/messages/prune.ts +++ b/lib/messages/prune.ts @@ -6,7 +6,7 @@ import { loadPrompt } from "../prompt" const PRUNED_TOOL_OUTPUT_REPLACEMENT = '[Output removed to save context - information superseded or no longer needed]' const NUDGE_STRING = loadPrompt("nudge") -const RECALL_STRING = loadPrompt("recall") +const RECALL_REMINDER_STRING = loadPrompt("recall-reminder") const buildPrunableToolsList = ( state: SessionState, @@ -68,9 +68,9 @@ export const insertPruneToolContext = ( } let recallString = "" - if (state.recallCounter >= config.strategies.pruneTool.recall.frequency) { - logger.info("Inserting prune recall message") - recallString = "\n" + RECALL_STRING + if (config.strategies.pruneTool.recall.enabled && state.recallCounter >= config.strategies.pruneTool.recall.frequency) { + logger.info("Inserting recall reminder") + recallString = "\n" + RECALL_REMINDER_STRING state.recallCounter = 0 } diff --git a/lib/prompts/recall-reminder.txt b/lib/prompts/recall-reminder.txt new file mode 100644 index 00000000..f801a106 --- /dev/null +++ b/lib/prompts/recall-reminder.txt @@ -0,0 +1 @@ +Consider using the `recall` tool to pause and reflect on your current progress, understanding, and next steps. diff --git a/lib/prompts/recall-tool.txt b/lib/prompts/recall-tool.txt new file mode 100644 index 00000000..14a4c471 --- /dev/null +++ b/lib/prompts/recall-tool.txt @@ -0,0 +1,26 @@ +Use this tool to pause and conduct a structured self-assessment of your current progress. + +## IMPORTANT: Provide your summary as the `summary` parameter to this tool + +When calling this tool, you MUST provide a comprehensive summary as the `summary` parameter that includes: + +### Task Understanding +- Key facts, data points, and domain knowledge you've acquired +- Assumptions you've made and any that have been validated or disproven +- Gaps in your understanding that still need to be addressed +- Connections between different pieces of information and how they inform the approach + +### Progress Evaluation +- What specific milestones have you completed? +- Which paths forward seem most viable based on current knowledge? + +### Organization +- Clearly separate established knowledge from open questions +- Distinguish completed work from remaining challenges +- Explicitly state what should happen next and why + +## Usage Notes + +- Write your structured self-assessment directly in the `summary` parameter when calling this tool +- After calling this tool, continue with your work - do not stop the agentic loop +- Use this as a checkpoint to ensure you're on the right track and haven't missed anything diff --git a/lib/state/tool-cache.ts b/lib/state/tool-cache.ts index 0c1fffdc..0242601a 100644 --- a/lib/state/tool-cache.ts +++ b/lib/state/tool-cache.ts @@ -27,7 +27,8 @@ export async function syncToolCache( if (part.tool === "prune") { state.nudgeCounter = 0 - + } else if (part.tool === "recall") { + state.recallCounter = 0 } else if (!config.strategies.pruneTool.protectedTools.includes(part.tool)) { state.nudgeCounter++ state.recallCounter++ diff --git a/lib/strategies/index.ts b/lib/strategies/index.ts index 105d9c81..dc5086ac 100644 --- a/lib/strategies/index.ts +++ b/lib/strategies/index.ts @@ -1,3 +1,4 @@ export { deduplicate } from "./deduplication" export { runOnIdle } from "./on-idle" export { createPruneTool } from "./prune-tool" +export { createRecallTool } from "./recall-tool" diff --git a/lib/strategies/recall-tool.ts b/lib/strategies/recall-tool.ts new file mode 100644 index 00000000..8801afc1 --- /dev/null +++ b/lib/strategies/recall-tool.ts @@ -0,0 +1,39 @@ +import { tool } from "@opencode-ai/plugin" +import type { SessionState } from "../state" +import type { Logger } from "../logger" +import { loadPrompt } from "../prompt" + +/** Tool description loaded from prompts/recall-tool.txt */ +const TOOL_DESCRIPTION = loadPrompt("recall-tool") + +export interface RecallToolContext { + state: SessionState + logger: Logger +} + +/** + * Creates the recall tool definition. + * Allows the LLM to pause and reflect on current progress and understanding. + */ +export function createRecallTool( + ctx: RecallToolContext, +): ReturnType { + return tool({ + description: TOOL_DESCRIPTION, + args: { + summary: tool.schema.string().describe( + "Your structured self-assessment including: task understanding (key facts, assumptions, gaps, connections), progress evaluation (completed milestones, viable paths forward), and next steps" + ), + }, + async execute(args, _toolCtx) { + const { state, logger } = ctx + + // Reset recall counter when recall is explicitly called + state.recallCounter = 0 + + logger.debug("Recall tool executed with summary:", args.summary?.substring(0, 100)) + + return "Recall completed. Continue with your work." + }, + }) +}