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,