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
4 changes: 3 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
createCompressRangeTool,
createDecompressTool,
createSearchContextTool,
createSetCompressionProfileTool,
} from "./lib/compress"
import {
compressDisabledByOpencode,
Expand Down Expand Up @@ -91,6 +92,7 @@ const server: Plugin = (async (ctx) => {
: createCompressRangeTool(compressToolContext),
decompress: createDecompressTool(compressToolContext),
search_context: createSearchContextTool(compressToolContext),
set_compression_profile: createSetCompressionProfileTool(compressToolContext),
}),
},
config: async (opencodeConfig) => {
Expand All @@ -111,7 +113,7 @@ const server: Plugin = (async (ctx) => {

const toolsToAdd: string[] = []
if (config.compress.permission !== "deny" && !config.experimental.allowSubAgents) {
toolsToAdd.push("compress", "decompress", "search_context")
toolsToAdd.push("compress", "decompress", "search_context", "set_compression_profile")
}

if (toolsToAdd.length > 0) {
Expand Down
2 changes: 2 additions & 0 deletions lib/compress/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,5 @@ export { createCompressMessageTool } from "./message"
export { createCompressRangeTool } from "./range"
export { createDecompressTool } from "./decompress"
export { createSearchContextTool } from "./search"
export { createSetCompressionProfileTool, getSessionProfile, PROFILES } from "./profile"
export type { CompressionProfile } from "./profile"
110 changes: 110 additions & 0 deletions lib/compress/profile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { tool } from "@opencode-ai/plugin"
import type { ToolContext } from "./types"
import * as fs from "fs"
import * as path from "path"

export type CompressionProfile = "aggressive" | "balanced" | "conservative"

export const PROFILES: Record<CompressionProfile, {
normalHint: string
summaryLimit: number
protectedItems: string
compressFrequency: number
largeOutputThreshold: number
}> = {
aggressive: {
normalHint: "Compress frequently — keep only final outcomes and key results. Context is disposable for short tasks.",
summaryLimit: 100,
protectedItems: "Protect only user instructions.",
compressFrequency: 4,
largeOutputThreshold: 2000,
},
balanced: {
normalHint: "After completing a task or sub-task, compress its tool outputs into summaries. Do NOT compress content you're actively using for an ongoing task.",
summaryLimit: 200,
protectedItems: "Protect user instructions, key decisions, file paths, and important findings.",
compressFrequency: 6,
largeOutputThreshold: 5000,
},
conservative: {
normalHint: "Compress ONLY verbose logs and obvious duplicates. NEVER compress experiment results, metrics, architectural decisions, code structure, or file paths.",
summaryLimit: 400,
protectedItems: "Protect everything in balanced PLUS: experiment results (PPL, accuracy, loss), metrics, code structure, previous experiment comparisons.",
compressFrequency: 10,
largeOutputThreshold: 10000,
},
}

export function getSessionConfigPath(sessionId: string): string {
const configDir = path.join(process.env.HOME || "~", ".config", "opencode", "acp-status")
return path.join(configDir, `${sessionId}.json`)
}

export function getSessionProfile(sessionId: string, defaultProfile: CompressionProfile): CompressionProfile {
try {
const filePath = getSessionConfigPath(sessionId)
if (fs.existsSync(filePath)) {
const data = JSON.parse(fs.readFileSync(filePath, "utf-8"))
if (data.compressionProfile && data.compressionProfile in PROFILES) {
return data.compressionProfile as CompressionProfile
}
}
} catch {}
return defaultProfile
}

export function setSessionProfile(sessionId: string, profile: CompressionProfile): void {
const filePath = getSessionConfigPath(sessionId)
const configDir = path.dirname(filePath)
if (!fs.existsSync(configDir)) {
fs.mkdirSync(configDir, { recursive: true })
}

let existing: Record<string, unknown> = {}
try {
if (fs.existsSync(filePath)) {
existing = JSON.parse(fs.readFileSync(filePath, "utf-8"))
}
} catch {}

existing.compressionProfile = profile
fs.writeFileSync(filePath, JSON.stringify(existing, null, 2), "utf-8")
}

export function createSetCompressionProfileTool(ctx: ToolContext): ReturnType<typeof tool> {
return tool({
description: "Set the compression profile for this session. Use 'conservative' for long experiments, 'aggressive' for short tasks, 'balanced' for general development.",
args: {
profile: tool.schema
.string()
.describe("Compression profile: 'aggressive' (short tasks), 'balanced' (default), 'conservative' (long experiments)")
.optional(),
},
async execute(args, toolCtx) {
const sessionId = ctx.state.sessionId || toolCtx?.sessionID || "unknown"

if (!args.profile) {
const current = getSessionProfile(sessionId, "balanced")
const config = PROFILES[current]
return `Current compression profile: ${current}\n` +
`Summary limit: ${config.summaryLimit} chars\n` +
`Compress frequency: every ${config.compressFrequency} turns\n` +
`Available: aggressive, balanced, conservative`
}

const profile = args.profile as CompressionProfile
if (!(profile in PROFILES)) {
throw new Error(`Invalid profile: ${profile}. Use: aggressive, balanced, or conservative`)
}

setSessionProfile(sessionId, profile)
const config = PROFILES[profile]

return `✅ Compression profile set to: ${profile}\n` +
`Summary limit: ${config.summaryLimit} chars\n` +
`Compress frequency: every ${config.compressFrequency} turns\n` +
`Large output threshold: ${config.largeOutputThreshold} chars\n` +
`${config.normalHint}`
},
})
}
12 changes: 12 additions & 0 deletions lib/config-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export const VALID_CONFIG_KEYS = new Set([
"gc.batchCleanup.lowThreshold",
"gc.batchCleanup.highThreshold",
"gc.batchCleanup.forceThreshold",
"compressionProfile",
"strategies",
"strategies.deduplication",
"strategies.deduplication.enabled",
Expand Down Expand Up @@ -107,6 +108,17 @@ export function validateConfigTypes(config: Record<string, any>): ValidationErro
errors.push({ key: "debug", expected: "boolean", actual: typeof config.debug })
}

if (config.compressionProfile !== undefined) {
const validProfiles = ["aggressive", "balanced", "conservative"]
if (!validProfiles.includes(config.compressionProfile)) {
errors.push({
key: "compressionProfile",
expected: '"aggressive" | "balanced" | "conservative"',
actual: JSON.stringify(config.compressionProfile),
})
}
}

if (config.pruneNotification !== undefined) {
const validValues = ["off", "minimal", "detailed"]
if (!validValues.includes(config.pruneNotification)) {
Expand Down
13 changes: 11 additions & 2 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { join, dirname } from "path"
import { homedir } from "os"
import { parse } from "jsonc-parser/lib/esm/main.js"
import type { PluginInput } from "@opencode-ai/plugin"
import type { CompressionProfile } from "./compress/profile"
import { VALID_CONFIG_KEYS, getInvalidConfigKeys, validateConfigTypes, type ValidationError } from "./config-validation"


Expand Down Expand Up @@ -89,6 +90,7 @@ export interface PluginConfig {
protectedFilePatterns: string[]
compress: CompressConfig
gc: GCConfig
compressionProfile: CompressionProfile
strategies: {
deduplication: Deduplication
purgeErrors: PurgeErrors
Expand Down Expand Up @@ -201,6 +203,7 @@ const defaultConfig: PluginConfig = {
maxSummaryLengthHard: 800,
minCompressRange: 2000,
},
compressionProfile: "balanced",
strategies: {
deduplication: {
enabled: true,
Expand Down Expand Up @@ -500,8 +503,11 @@ function mergeGC(base: GCConfig, override?: Partial<GCConfig>): GCConfig {
}
}

function mergeLayer(config: PluginConfig, data: Record<string, any>): PluginConfig {
return {
function isValidCompressionProfile(value: unknown): value is CompressionProfile {
return value === "aggressive" || value === "balanced" || value === "conservative"
}

function mergeLayer(config: PluginConfig, data: Record<string, any>): PluginConfig { return {
enabled: data.enabled ?? config.enabled,
autoUpdate: data.autoUpdate ?? config.autoUpdate,
debug: data.debug ?? config.debug,
Expand All @@ -519,6 +525,9 @@ function mergeLayer(config: PluginConfig, data: Record<string, any>): PluginConf
],
compress: mergeCompress(config.compress, data.compress as CompressOverride),
gc: mergeGC(config.gc, data.gc as Partial<GCConfig>),
compressionProfile: isValidCompressionProfile(data.compressionProfile)
? data.compressionProfile
: config.compressionProfile,
strategies: mergeStrategies(config.strategies, data.strategies as any),
}
}
Expand Down
8 changes: 7 additions & 1 deletion lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@ import {
computeInputBudget,
} from "./messages"
import { renderSystemPrompt, type PromptStore } from "./prompts"
import { buildSystemPrompt } from "./prompts/system"
import { buildProtectedToolsExtension } from "./prompts/extensions/system"
import {
applyPendingCompressionDurations,
buildCompressionTimingKey,
consumeCompressionStart,
resolveCompressionDuration,
} from "./compress/timing"
import { getSessionProfile } from "./compress/profile"
import { filterMessages, filterMessagesInPlace } from "./messages/shape"
import { getLastUserMessage } from "./messages/query"
import {
Expand Down Expand Up @@ -107,8 +109,12 @@ export function createSystemPromptHandler(

prompts.reload()
const runtimePrompts = prompts.getRuntimePrompts()
const effectiveProfile = state.sessionId
? getSessionProfile(state.sessionId, config.compressionProfile)
: config.compressionProfile
const profileSystem = `<dcp-system-reminder>\n${buildSystemPrompt(effectiveProfile).trim()}\n</dcp-system-reminder>`
const newPrompt = renderSystemPrompt(
runtimePrompts,
{ ...runtimePrompts, system: profileSystem },
buildProtectedToolsExtension(config.compress.protectedTools),
!!state.manualMode,
state.isSubAgent && config.experimental.allowSubAgents,
Expand Down
19 changes: 17 additions & 2 deletions lib/prompts/system.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
export const SYSTEM = `
import type { CompressionProfile } from "../compress/profile"
import { PROFILES } from "../compress/profile"

export function buildSystemPrompt(profile: CompressionProfile = "balanced"): string {
const config = PROFILES[profile]
return `

You operate in a context-constrained environment. Context management helps preserve retrieval quality, but your primary goal is completing the task at hand. Do not let context management distract from the actual work.

The tools you have for context management are \`compress\`, \`decompress\`, and \`search_context\`. \`compress\` replaces older conversation content with technical summaries you produce. \`decompress\` restores previously compressed content when you need exact details. \`search_context\` searches compressed block summaries (and visible messages) to locate relevant content before you decompress.
The tools you have for context management are \`compress\`, \`decompress\`, \`search_context\`, and \`set_compression_profile\`. \`compress\` replaces older conversation content with technical summaries you produce. \`decompress\` restores previously compressed content when you need exact details. \`search_context\` searches compressed block summaries (and visible messages) to locate relevant content before you decompress. \`set_compression_profile\` adjusts how aggressively this session compresses content.

\`<dcp-message-id>\` and \`<dcp-system-reminder>\` tags are environment-injected metadata. Do not output them.

ACTIVE COMPRESSION MODE: ${profile.toUpperCase()}

${config.normalHint}

Summary limit: ${config.summaryLimit} chars.
${config.protectedItems}

COMPRESSION PHILOSOPHY

Compression replaces raw conversation content with dense summaries. When used correctly, it keeps your context sharp and focused. When used carelessly, it destroys information you need.
Expand Down Expand Up @@ -70,3 +82,6 @@ Use \`search_context\` to find relevant compressed content before decompressing

Use \`compress\` and \`decompress\` deliberately with quality-first summaries. Prioritize stale content intelligently to maintain a high-signal context window.
`
}

export const SYSTEM = buildSystemPrompt("balanced")
Loading