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
2 changes: 1 addition & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
81 changes: 40 additions & 41 deletions lib/prompts/compress-tool-spec.ts
Original file line number Diff line number Diff line change
@@ -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.

<example_compress>
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"
]
]
</example_compress>
## Best Practices

<example_keep>
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.
</example_keep>`
- **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`
75 changes: 47 additions & 28 deletions lib/prompts/distill-tool-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,64 @@ export const DISTILL_TOOL_SPEC = `Distills key findings from tool outputs into p
## IMPORTANT: The Prunable List
A \`<prunable-tools>\` 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 \`<prunable-tools>\` 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 \`<prunable-tools>\` 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

<example_distillation>
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' }"
]
]
</example_distillation>
## Best Practices

<example_keep>
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.
</example_keep>`
- **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.)`
61 changes: 39 additions & 22 deletions lib/prompts/prune-tool-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,54 @@ export const PRUNE_TOOL_SPEC = `Prunes tool outputs from context to manage conve
## IMPORTANT: The Prunable List
A \`<prunable-tools>\` 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 \`<prunable-tools>\` list
- \`ids\` Array of numeric IDs as strings from the \`<prunable-tools>\` list

## Example

<example_noise>
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"]]
</example_noise>
## Best Practices

<example_superseded>
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"]]
</example_superseded>`
- **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.`
68 changes: 29 additions & 39 deletions lib/tools/compress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof tool> {
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")
Expand All @@ -56,14 +62,6 @@ export function createCompressTool(ctx: PruneToolContext): ReturnType<typeof too
}

logger.info("Compress tool invoked")
// logger.info(
// JSON.stringify({
// startString: startString?.substring(0, 50) + "...",
// endString: endString?.substring(0, 50) + "...",
// topic: topic,
// summaryLength: summary?.length,
// }),
// )

const messagesResponse = await client.session.messages({
path: { id: sessionId },
Expand All @@ -74,14 +72,14 @@ export function createCompressTool(ctx: PruneToolContext): ReturnType<typeof too

const startResult = findStringInMessages(
messages,
startString,
startMarker,
logger,
state.compressSummaries,
"startString",
)
const endResult = findStringInMessages(
messages,
endString,
endMarker,
logger,
state.compressSummaries,
"endString",
Expand Down Expand Up @@ -163,14 +161,6 @@ export function createCompressTool(ctx: PruneToolContext): ReturnType<typeof too
state.stats.pruneTokenCounter = 0
state.nudgeCounter = 0

// logger.info("Compress range created", {
// startMessageId: startResult.messageId,
// endMessageId: endResult.messageId,
// toolIdsRemoved: containedToolIds.length,
// messagesInRange: containedMessageIds.length,
// estimatedTokens: estimatedCompressedTokens,
// })

saveSessionState(state, logger).catch((err) =>
logger.error("Failed to persist state", { error: err.message }),
)
Expand Down
Loading