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 eb90adc..c9332b9 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 {
@@ -40,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([
@@ -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..cd2bc6d 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_REMINDER_STRING = loadPrompt("recall-reminder")
const buildPrunableToolsList = (
state: SessionState,
@@ -66,6 +67,13 @@ export const insertPruneToolContext = (
nudgeString = "\n" + NUDGE_STRING
}
+ let recallString = ""
+ 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
+ }
+
const userMessage: WithParts = {
info: {
id: "msg_01234567890123456789012345",
@@ -84,7 +92,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-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/prompts/recall.txt b/lib/prompts/recall.txt
new file mode 100644
index 0000000..f78c183
--- /dev/null
+++ b/lib/prompts/recall.txt
@@ -0,0 +1,16 @@
+
+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.
+
+DO NOT BREAK AGENTIC LOOP - KEEP GOING AFTER THIS ACTION
+
\ 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..0242601 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
+ } else if (part.tool === "recall") {
+ 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/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."
+ },
+ })
+}
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')