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 06b370e..c63b612 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 eb90adc..d6d465e 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 7361b74..ded62f6 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 0000000..07d3cff --- /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 91e3f92..ccf09ca 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 a6140c7..489fc22 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 e1b92a7..d6b6a8e 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 49887d3..0a2fdc1 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 c546363..cf309f8 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 ded62f6..c87f182 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 489fc22..0c1fffd 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 0a2fdc1..49887d3 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 cf309f8..c546363 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 07d3cff..f78c183 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 6b617c4..4cb25be 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 d6d465e..c9332b9 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 c87f182..cd2bc6d 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 0000000..f801a10 --- /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 0000000..14a4c47 --- /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 0c1fffd..0242601 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 105d9c8..dc5086a 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 0000000..8801afc --- /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." + }, + }) +}