Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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),
Expand Down
26 changes: 24 additions & 2 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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([
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -230,6 +239,10 @@ const defaultConfig: PluginConfig = {
nudge: {
enabled: true,
frequency: 10
},
recall: {
enabled: true,
frequency: 10
}
},
onIdle: {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
}
Expand All @@ -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 }
}
}
}
Expand Down
10 changes: 9 additions & 1 deletion lib/messages/prune.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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",
Expand All @@ -84,7 +92,7 @@ export const insertPruneToolContext = (
sessionID: lastUserMessage.info.sessionID,
messageID: "msg_01234567890123456789012345",
type: "text",
text: prunableToolsList + nudgeString,
text: prunableToolsList + nudgeString + recallString,
}
]
}
Expand Down
1 change: 1 addition & 0 deletions lib/prompts/recall-reminder.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Consider using the `recall` tool to pause and reflect on your current progress, understanding, and next steps.
26 changes: 26 additions & 0 deletions lib/prompts/recall-tool.txt
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions lib/prompts/recall.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<action name=recall policy=critical>
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
</action>
2 changes: 2 additions & 0 deletions lib/state/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export function createSessionState(): SessionState {
},
toolParameters: new Map<string, ToolParameterEntry>(),
nudgeCounter: 0,
recallCounter: 0,
lastToolPrune: false
}
}
Expand All @@ -57,6 +58,7 @@ export function resetSessionState(state: SessionState): void {
}
state.toolParameters.clear()
state.nudgeCounter = 0
state.recallCounter = 0
state.lastToolPrune = false
}

Expand Down
6 changes: 5 additions & 1 deletion lib/state/tool-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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"

Expand Down
1 change: 1 addition & 0 deletions lib/state/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,6 @@ export interface SessionState {
stats: SessionStats
toolParameters: Map<string, ToolParameterEntry>
nudgeCounter: number
recallCounter: number
lastToolPrune: boolean
}
1 change: 1 addition & 0 deletions lib/strategies/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { deduplicate } from "./deduplication"
export { runOnIdle } from "./on-idle"
export { createPruneTool } from "./prune-tool"
export { createRecallTool } from "./recall-tool"
39 changes: 39 additions & 0 deletions lib/strategies/recall-tool.ts
Original file line number Diff line number Diff line change
@@ -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<typeof tool> {
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."
},
})
}
2 changes: 1 addition & 1 deletion lib/ui/notification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down