diff --git a/index.ts b/index.ts
index d066fac8..5fdca598 100644
--- a/index.ts
+++ b/index.ts
@@ -2,7 +2,7 @@ import type { Plugin } from "@opencode-ai/plugin"
import { getConfig } from "./lib/config"
import { Logger } from "./lib/logger"
import { createSessionState } from "./lib/state"
-import { createPruneTool, createDistillTool, createCompressTool } from "./lib/strategies"
+import { createPruneTool, createDistillTool, createCompressTool } from "./lib/tools"
import {
createChatMessageTransformHandler,
createCommandExecuteHandler,
diff --git a/lib/prompts/compress-tool-spec.ts b/lib/prompts/compress-tool-spec.ts
index 0a08b08f..38ff5e84 100644
--- a/lib/prompts/compress-tool-spec.ts
+++ b/lib/prompts/compress-tool-spec.ts
@@ -1,57 +1,56 @@
export const COMPRESS_TOOL_SPEC = `Collapses a contiguous range of conversation into a single summary.
+## Quick Examples
+
+\`\`\`javascript
+// Example 1: Compress after completing a research phase
+compress({
+ startMarker: "I'll explore the auth system",
+ endMarker: "Found JWT tokens with 24h expiry",
+ topic: "Auth System Exploration",
+ summary: "Auth: JWT 24h expiry, bcrypt passwords, refresh rotation. Files: auth.ts, tokens.ts, middleware/auth.ts"
+})
+\`\`\`
+
+\`\`\`javascript
+// Example 2: Compress after debugging and fixing errors
+compress({
+ startMarker: "Starting to debug the login error",
+ endMarker: "Login now works correctly",
+ topic: "Login Debug Session",
+ summary: "Fixed JWT token parsing issue. Problem: missing authorization header. Solution: added auth header to API calls."
+})
+\`\`\`
+
## When to Use This Tool
Use \`compress\` when you want to condense an entire sequence of work into a brief summary:
-- **Phase Completion:** You completed a phase (research, tool calls, implementation) and want to collapse the entire sequence into a summary.
-- **Exploration Done:** You explored multiple files or ran multiple commands and only need a summary of what you learned.
-- **Failed Attempts:** You tried several unsuccessful approaches and want to condense them into a brief note.
-- **Verbose Output:** A section of conversation has grown large but can be summarized without losing critical details.
+- **Phase Completion:** You completed a phase (research, tool calls, implementation) and want to collapse the entire sequence into a summary
+- **Exploration Done:** You explored multiple files or ran multiple commands and only need a summary of what you learned
+- **Failed Attempts:** You tried several unsuccessful approaches and want to condense them into a brief note
+- **Verbose Output:** A section of conversation has grown large but can be summarized without losing critical details
## When NOT to Use This Tool
-- **If you need specific details:** If you'll need exact code, file contents, or error messages from the range, keep them.
-- **For individual tool outputs:** Use \`prune\` or \`distill\` for single tool outputs. Compress targets conversation ranges.
-- **If it's recent content:** You may still need recent work for the current phase.
-
-## How It Works
-
-1. \`startString\` — A unique text string that marks the start of the range to compress
-2. \`endString\` — A unique text string that marks the end of the range to compress
-3. \`topic\` — A short label (3-5 words) describing the compressed content
-4. \`summary\` — The replacement text that will be inserted
-
-Everything between startString and endString (inclusive) is removed and replaced with your summary.
-
-**Important:** The compress will FAIL if \`startString\` or \`endString\` is not found in the conversation. The compress will also FAIL if either string is found multiple times. Provide a larger string with more surrounding context to uniquely identify the intended match.
-
-## Best Practices
-- **Choose unique strings:** Pick text that appears only once in the conversation.
-- **Write concise topics:** Examples: "Auth System Exploration", "Token Logic Refactor"
-- **Write comprehensive summaries:** Include key information like file names, function signatures, and important findings.
-- **Timing:** Best used after finishing a work phase, not during active exploration.
+- **If you need specific details:** If you'll need exact code, file contents, or error messages from the range, keep them
+- **For individual tool outputs:** Use \`prune\` or \`distill\` for single tool outputs. Compress targets conversation ranges
+- **If it's recent content:** You may still need recent work for the current phase
## Format
-- \`input\`: Array with four elements: [startString, endString, topic, summary]
+- \`startMarker\` — A unique text string that marks the start of the range to compress
+- \`endMarker\` — A unique text string that marks the end of the range to compress
+- \`topic\` — A short label (3-5 words) describing the compressed content
+- \`summary\` — The replacement text that will be inserted
-## Example
+Everything between startMarker and endMarker (inclusive) is removed and replaced with your summary.
-
-Conversation: [Asked about auth] -> [Read 5 files] -> [Analyzed patterns] -> [Found "JWT tokens with 24h expiry"]
+**Important:** The compress will FAIL if \`startMarker\` or \`endMarker\` is not found in the conversation. The compress will also FAIL if either string is found multiple times. Provide a larger string with more surrounding context to uniquely identify the intended match.
-[Uses compress with:
- input: [
- "Asked about authentication",
- "JWT tokens with 24h expiry",
- "Auth System Exploration",
- "Auth: JWT 24h expiry, bcrypt passwords, refresh rotation. Files: auth.ts, tokens.ts, middleware/auth.ts"
- ]
-]
-
+## Best Practices
-
-Assistant: [Just finished reading auth.ts]
-I've read the auth file and now need to make edits based on it. I'm keeping this in context rather than compressing.
-`
+- **Choose unique strings:** Pick text that appears only once in the conversation
+- **Write concise topics:** Examples: "Auth System Exploration", "Token Logic Refactor"
+- **Write comprehensive summaries:** Include key information like file names, function signatures, and important findings
+- **Timing:** Best used after finishing a work phase, not during active exploration`
diff --git a/lib/prompts/distill-tool-spec.ts b/lib/prompts/distill-tool-spec.ts
index 9fccc048..50b86088 100644
--- a/lib/prompts/distill-tool-spec.ts
+++ b/lib/prompts/distill-tool-spec.ts
@@ -3,45 +3,64 @@ export const DISTILL_TOOL_SPEC = `Distills key findings from tool outputs into p
## IMPORTANT: The Prunable List
A \`\` list is provided to you showing available tool outputs you can distill from when there are tools available for pruning. Each line has the format \`ID: tool, parameter\` (e.g., \`20: read, /path/to/file.ts\`). You MUST only use numeric IDs that appear in this list to select which tools to distill.
+## Quick Examples
+
+\`\`\`javascript
+// Example 1: Distill technical details from file reads
+distill({
+ items: [
+ {
+ id: "10",
+ distillation: "auth.ts: validateToken(token: string) -> User|null checks cache first (5min TTL) then OIDC. hashPassword uses bcrypt 12 rounds. Tokens must be 128+ chars."
+ },
+ {
+ id: "11",
+ distillation: "user.ts: interface User { id: string; email: string; permissions: ('read'|'write'|'admin')[]; status: 'active'|'suspended' }"
+ }
+ ]
+})
+\`\`\`
+
+\`\`\`javascript
+// Example 2: Distill findings from multiple grep searches
+distill({
+ items: [
+ {
+ id: "15",
+ distillation: "Found 3 API endpoints: POST /api/login, GET /api/users, DELETE /api/users/:id. All require JWT authentication."
+ },
+ {
+ id: "16",
+ distillation: "Found error handling middleware in middleware/errors.ts. Logs errors to file, sends sanitized error response to client."
+ }
+ ]
+})
+\`\`\`
+
## When to Use This Tool
Use \`distill\` when you have individual tool outputs with valuable information you want to **preserve in distilled form** before removing the raw content:
-- **Large Outputs:** The raw output is too large but contains valuable technical details worth keeping.
-- **Knowledge Preservation:** You have context that contains valuable information (signatures, logic, constraints) but also a lot of unnecessary detail.
+- **Large Outputs:** The raw output is too large but contains valuable technical details worth keeping
+- **Knowledge Preservation:** You have context that contains valuable information (signatures, logic, constraints) but also a lot of unnecessary detail
+- **Multiple similar operations:** After running several related commands (like multiple grep searches), preserve the consolidated findings
## When NOT to Use This Tool
-- **If you need precise syntax:** If you'll edit a file or grep for exact strings, keep the raw output.
-- **If uncertain:** Prefer keeping over re-fetching.
-
-
-## Best Practices
-- **Strategic Batching:** Wait until you have several items or a few large outputs to distill, rather than doing tiny, frequent distillations. Aim for high-impact distillations that significantly reduce context size.
-- **Think ahead:** Before distilling, ask: "Will I need the raw output for upcoming work?" If you researched a file you'll later edit, do NOT distill it.
+- **If you need precise syntax:** If you'll edit a file or grep for exact strings, keep the raw output
+- **If uncertain:** Prefer keeping over re-fetching
+- **For noise removal:** Use \`prune\` for irrelevant or superseded outputs
## Format
-- \`ids\`: Array of numeric IDs as strings from the \`\` list
-- \`distillation\`: Array of strings, one per ID (positional: distillation[0] is for ids[0], etc.)
+- \`items\` — Array of objects, each containing:
+ - \`id\` — Numeric ID as string from the \`\` list
+ - \`distillation\` — String capturing the essential information to preserve
Each distillation string should capture the essential information you need to preserve - function signatures, logic, constraints, values, etc. Be as detailed as needed.
-## Example
-
-
-Assistant: [Reads auth service and user types]
-I'll preserve the key details before distilling.
-[Uses distill with:
- ids: ["10", "11"],
- distillation: [
- "auth.ts: validateToken(token: string) -> User|null checks cache first (5min TTL) then OIDC. hashPassword uses bcrypt 12 rounds. Tokens must be 128+ chars.",
- "user.ts: interface User { id: string; email: string; permissions: ('read'|'write'|'admin')[]; status: 'active'|'suspended' }"
- ]
-]
-
+## Best Practices
-
-Assistant: [Reads 'auth.ts' to understand the login flow]
-I've understood the auth flow. I'll need to modify this file to add the new validation, so I'm keeping this read in context rather than distilling.
-`
+- **Strategic Batching:** Wait until you have several items or a few large outputs to distill, rather than doing tiny, frequent distillations. Aim for high-impact distillations that significantly reduce context size.
+- **Think ahead:** Before distilling, ask: "Will I need the raw output for upcoming work?" If you researched a file you'll later edit, do NOT distill it.
+- **Focus on essentials:** Capture what you'll need to recall later (signatures, behaviors, constraints) without unnecessary detail (exact formatting, whitespace, etc.)`
diff --git a/lib/prompts/prune-tool-spec.ts b/lib/prompts/prune-tool-spec.ts
index c2ea3cbb..903416ab 100644
--- a/lib/prompts/prune-tool-spec.ts
+++ b/lib/prompts/prune-tool-spec.ts
@@ -3,37 +3,54 @@ export const PRUNE_TOOL_SPEC = `Prunes tool outputs from context to manage conve
## IMPORTANT: The Prunable List
A \`\` list is provided to you showing available tool outputs you can prune when there are tools available for pruning. Each line has the format \`ID: tool, parameter\` (e.g., \`20: read, /path/to/file.ts\`). You MUST only use numeric IDs that appear in this list to select which tools to prune.
+## Quick Examples
+
+\`\`\`javascript
+// Example 1: Prune irrelevant file read
+prune({
+ ids: ["5"]
+})
+// Context: Read 'wrong_file.ts' which wasn't relevant to the auth system
+\`\`\`
+
+\`\`\`javascript
+// Example 2: Prune multiple outdated reads in batch
+prune({
+ ids: ["20", "23", "27"]
+})
+// Context: Read config.ts three times, keeping only the most recent version
+\`\`\`
+
+\`\`\`javascript
+// Example 3: Prune irrelevant search results
+prune({
+ ids: ["15", "16", "17"]
+})
+// Context: Three grep searches that returned no useful results
+\`\`\`
+
## When to Use This Tool
Use \`prune\` for removing individual tool outputs that are no longer needed:
-- **Noise:** Irrelevant, unhelpful, or superseded outputs that provide no value.
-- **Wrong Files:** You read or accessed something that turned out to be irrelevant.
-- **Outdated Info:** Outputs that have been superseded by newer information.
+- **Noise:** Irrelevant, unhelpful, or superseded outputs that provide no value
+- **Wrong Files:** You read or accessed something that turned out to be irrelevant
+- **Outdated Info:** Outputs that have been superseded by newer information
+- **Failed Commands:** Commands that failed and won't be retried
## When NOT to Use This Tool
-- **If the output contains useful information:** Keep it in context rather than pruning.
-- **If you'll need the output later:** Don't prune files you plan to edit or context you'll need for implementation.
-
-## Best Practices
-- **Strategic Batching:** Don't prune single small tool outputs (like short bash commands) unless they are pure noise. Wait until you have several items to perform high-impact prunes.
-- **Think ahead:** Before pruning, ask: "Will I need this output for upcoming work?" If yes, keep it.
+- **If the output contains useful information:** Keep it in context rather than pruning
+- **If you'll need the output later:** Don't prune files you plan to edit or context you'll need for implementation
+- **For preserving knowledge:** Use \`distill\` if you want to save key insights before removing
+- **For conversation ranges:** Use \`compress\` to collapse multiple messages at once
## Format
-- \`ids\`: Array of numeric IDs as strings from the \`\` list
+- \`ids\` — Array of numeric IDs as strings from the \`\` list
-## Example
-
-
-Assistant: [Reads 'wrong_file.ts']
-This file isn't relevant to the auth system. I'll remove it to clear the context.
-[Uses prune with ids: ["5"]]
-
+## Best Practices
-
-Assistant: [Reads config.ts, then reads updated config.ts after changes]
-The first read is now outdated. I'll prune it and keep the updated version.
-[Uses prune with ids: ["20"]]
-`
+- **Strategic Batching:** Don't prune single small tool outputs (like short bash commands) unless they are pure noise. Wait until you have several items to perform high-impact prunes.
+- **Think ahead:** Before pruning, ask: "Will I need this output for upcoming work?" If yes, keep it.
+- **Consolidate operations:** Group multiple prunes into a single call when possible. It's rarely worth pruning one tiny tool output.`
diff --git a/lib/tools/compress.ts b/lib/tools/compress.ts
index 84b9f83f..437597ce 100644
--- a/lib/tools/compress.ts
+++ b/lib/tools/compress.ts
@@ -15,38 +15,44 @@ import { sendCompressNotification } from "../ui/notification"
const COMPRESS_TOOL_DESCRIPTION = loadPrompt("compress-tool-spec")
+/**
+ * Creates a tool for compressing contiguous ranges of conversation into summaries.
+ *
+ * This tool allows LLMs to collapse sequences of messages and tool outputs into
+ * concise topic+summary pairs, significantly reducing context size while preserving
+ * key information.
+ *
+ * @param ctx - The prune tool context containing client, state, and logger
+ * @returns A configured tool instance for the OpenCode plugin
+ */
export function createCompressTool(ctx: PruneToolContext): ReturnType {
return tool({
description: COMPRESS_TOOL_DESCRIPTION,
args: {
- input: tool.schema
- .array(tool.schema.string())
- .describe(
- "[startString, endString, topic, summary] - 4 required strings: (1) startString: unique text from conversation marking range start, (2) endString: unique text marking range end, (3) topic: short 3-5 word label for UI, (4) summary: comprehensive text replacing all compressed content",
- ),
+ startMarker: tool.schema
+ .string()
+ .describe("Unique text from conversation marking the start of the range to compress"),
+ endMarker: tool.schema
+ .string()
+ .describe("Unique text from conversation marking the end of the range to compress"),
+ topic: tool.schema
+ .string()
+ .describe("Short label (3-5 words) describing the compressed content for UI"),
+ summary: tool.schema
+ .string()
+ .describe("Comprehensive text that will replace all compressed content"),
},
async execute(args, toolCtx) {
const { client, state, logger } = ctx
const sessionId = toolCtx.sessionID
- if (!Array.isArray(args.input)) {
- throw new Error(
- 'input must be an array of 4 strings: ["startString", "endString", "topic", "summary"]',
- )
- }
- if (args.input.length !== 4) {
- throw new Error(
- `input must be an array of exactly 4 strings: ["startString", "endString", "topic", "summary"], got ${args.input.length} elements`,
- )
- }
+ const { startMarker, endMarker, topic, summary } = args
- const [startString, endString, topic, summary] = args.input
-
- if (!startString || typeof startString !== "string") {
- throw new Error("startString is required and must be a non-empty string")
+ if (!startMarker || typeof startMarker !== "string") {
+ throw new Error("startMarker is required and must be a non-empty string")
}
- if (!endString || typeof endString !== "string") {
- throw new Error("endString is required and must be a non-empty string")
+ if (!endMarker || typeof endMarker !== "string") {
+ throw new Error("endMarker is required and must be a non-empty string")
}
if (!topic || typeof topic !== "string") {
throw new Error("topic is required and must be a non-empty string")
@@ -56,14 +62,6 @@ export function createCompressTool(ctx: PruneToolContext): ReturnType
logger.error("Failed to persist state", { error: err.message }),
)
diff --git a/lib/tools/distill.ts b/lib/tools/distill.ts
index fd969392..f145dd51 100644
--- a/lib/tools/distill.ts
+++ b/lib/tools/distill.ts
@@ -6,62 +6,65 @@ import { loadPrompt } from "../prompts"
const DISTILL_TOOL_DESCRIPTION = loadPrompt("distill-tool-spec")
+/**
+ * Creates a tool for distilling key findings from tool outputs.
+ *
+ * This tool allows LLMs to preserve valuable information from large or
+ * complex tool outputs while removing the raw content to save context.
+ *
+ * @param ctx - The prune tool context containing logger and state
+ * @returns A configured tool instance for the OpenCode plugin
+ */
export function createDistillTool(ctx: PruneToolContext): ReturnType {
return tool({
description: DISTILL_TOOL_DESCRIPTION,
args: {
- ids: tool.schema
- .array(tool.schema.string())
- .describe("Numeric IDs as strings to distill from the list"),
- distillation: tool.schema
- .array(tool.schema.string())
- .describe(
- "Required array of distillation strings, one per ID (positional: distillation[0] for ids[0], etc.)",
- ),
+ items: tool.schema
+ .array(
+ tool.schema.object({
+ id: tool.schema.string().describe("Numeric ID as string from list"),
+ distillation: tool.schema
+ .string()
+ .describe("String capturing essential information to preserve"),
+ }),
+ )
+ .describe("Array of objects, each containing id and distillation"),
},
async execute(args, toolCtx) {
- if (!args.ids || !Array.isArray(args.ids) || args.ids.length === 0) {
- ctx.logger.debug("Distill tool called without ids: " + JSON.stringify(args))
- throw new Error("Missing ids. You must provide at least one ID to distill.")
- }
-
- if (!args.ids.every((id) => typeof id === "string" && id.trim() !== "")) {
- ctx.logger.debug("Distill tool called with invalid ids: " + JSON.stringify(args))
+ if (!args.items || !Array.isArray(args.items) || args.items.length === 0) {
+ ctx.logger.debug("Distill tool called without items: " + JSON.stringify(args))
throw new Error(
- 'Invalid ids. All IDs must be numeric strings (e.g., "1", "23") from the list.',
+ "Missing items. You must provide at least one item to distill.",
)
}
- if (
- !args.distillation ||
- !Array.isArray(args.distillation) ||
- args.distillation.length === 0
- ) {
- ctx.logger.debug(
- "Distill tool called without distillation: " + JSON.stringify(args),
- )
- throw new Error(
- 'Missing distillation. You must provide an array of strings (e.g., ["summary 1", "summary 2"]).',
- )
- }
+ const ids: string[] = []
+ const distillations: string[] = []
- if (!args.distillation.every((d) => typeof d === "string")) {
- ctx.logger.debug(
- "Distill tool called with non-string distillation: " + JSON.stringify(args),
- )
- throw new Error("Invalid distillation. All distillation entries must be strings.")
- }
+ for (const item of args.items) {
+ if (!item.id || typeof item.id !== "string" || item.id.trim() === "") {
+ ctx.logger.debug("Distill tool called with invalid id: " + JSON.stringify(args))
+ throw new Error(
+ 'Invalid id. All ids must be numeric strings (e.g., "1", "23") from the list.',
+ )
+ }
- // ctx.logger.info("Distillation data received:")
- // ctx.logger.info(JSON.stringify(args.distillation, null, 2))
+ if (!item.distillation || typeof item.distillation !== "string") {
+ ctx.logger.debug("Distill tool called with invalid distillation: " + JSON.stringify(args))
+ throw new Error("Invalid distillation. All distillation entries must be strings.")
+ }
+
+ ids.push(item.id)
+ distillations.push(item.distillation)
+ }
return executePruneOperation(
ctx,
toolCtx,
- args.ids,
+ ids,
"extraction" as PruneReason,
"Distill",
- args.distillation,
+ distillations,
)
},
})
diff --git a/lib/tools/prune.ts b/lib/tools/prune.ts
index 17065aa9..197cb445 100644
--- a/lib/tools/prune.ts
+++ b/lib/tools/prune.ts
@@ -6,6 +6,15 @@ import { loadPrompt } from "../prompts"
const PRUNE_TOOL_DESCRIPTION = loadPrompt("prune-tool-spec")
+/**
+ * Creates a tool for pruning tool outputs from the conversation context.
+ *
+ * This tool allows LLMs to remove individual tool outputs that are no longer needed,
+ * such as irrelevant information, outdated data, or superseded outputs.
+ *
+ * @param ctx - The prune tool context containing logger and state
+ * @returns A configured tool instance for the OpenCode plugin
+ */
export function createPruneTool(ctx: PruneToolContext): ReturnType {
return tool({
description: PRUNE_TOOL_DESCRIPTION,