From 4e2530c76c1ab6945ff4633f3c60a33ef82a3c2b Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Thu, 27 Nov 2025 21:02:57 -0500 Subject: [PATCH 1/2] feat: add reasoning block extraction and improve onTool strategy - Extract and log reasoning blocks from session messages for debugging - Include encrypted content size from OpenAI, Anthropic, and Google providers - Enable ai-analysis strategy by default on context_pruning tool calls - Handle reasoning type parts in message minimization - Fix README.md config key typo (plugins -> plugin) - Add comments explaining provider workarounds --- README.md | 2 +- index.ts | 21 ++++++++++++++++- lib/config.ts | 4 ++-- lib/logger.ts | 52 +++++++++++++++++++++++++++++++++++++++++-- lib/model-selector.ts | 3 ++- lib/prompt.ts | 22 ++++++++++++++++++ 6 files changed, 97 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 874c64d..6566672 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Add to your OpenCode config: ```jsonc // opencode.jsonc { - "plugins": ["@tarquinen/opencode-dcp@0.3.19"] + "plugin": ["@tarquinen/opencode-dcp@0.3.19"] } ``` diff --git a/index.ts b/index.ts index 2d78068..608882c 100644 --- a/index.ts +++ b/index.ts @@ -100,6 +100,24 @@ const plugin: Plugin = (async (ctx) => { }) if (logger.enabled) { + // Fetch session messages to extract reasoning blocks + let sessionMessages: any[] | undefined + try { + const activeSessions = allSessions.data?.filter(s => !s.parentID) || [] + if (activeSessions.length > 0) { + const mostRecentSession = activeSessions[0] + const messagesResponse = await ctx.client.session.messages({ + path: { id: mostRecentSession.id }, + query: { limit: 100 } + }) + sessionMessages = Array.isArray(messagesResponse.data) + ? messagesResponse.data + : Array.isArray(messagesResponse) ? messagesResponse : undefined + } + } catch (e) { + // Silently continue without session messages + } + await logger.saveWrappedContext( "global", body.messages, @@ -107,7 +125,8 @@ const plugin: Plugin = (async (ctx) => { url: typeof input === 'string' ? input : 'URL object', replacedCount, totalMessages: body.messages.length - } + }, + sessionMessages ) } diff --git a/lib/config.ts b/lib/config.ts index 60d5b59..b3e6e79 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -35,7 +35,7 @@ const defaultConfig: PluginConfig = { pruning_summary: 'detailed', strategies: { onIdle: ['deduplication', 'ai-analysis'], - onTool: ['deduplication'] + onTool: ['deduplication', 'ai-analysis'] } } @@ -114,7 +114,7 @@ function createDefaultConfig(): void { // Strategies to run when session goes idle "onIdle": ["deduplication", "ai-analysis"], // Strategies to run when AI calls context_pruning tool - "onTool": ["deduplication"] + "onTool": ["deduplication", "ai-analysis"] }, // Summary display: "off", "minimal", or "detailed" "pruning_summary": "detailed", diff --git a/lib/logger.ts b/lib/logger.ts index 5a97018..d51e888 100644 --- a/lib/logger.ts +++ b/lib/logger.ts @@ -147,7 +147,42 @@ export class Logger { return result } - async saveWrappedContext(sessionID: string, messages: any[], metadata: any) { + private extractReasoningBlocks(sessionMessages: any[]): any[] { + const reasoningBlocks: any[] = [] + + for (const msg of sessionMessages) { + if (!msg.parts) continue + + for (const part of msg.parts) { + if (part.type === "reasoning") { + // Calculate encrypted content size for different providers + let encryptedContentLength = 0 + if (part.metadata?.openai?.reasoningEncryptedContent) { + encryptedContentLength = part.metadata.openai.reasoningEncryptedContent.length + } else if (part.metadata?.anthropic?.signature) { + encryptedContentLength = part.metadata.anthropic.signature.length + } else if (part.metadata?.google?.thoughtSignature) { + encryptedContentLength = part.metadata.google.thoughtSignature.length + } + + reasoningBlocks.push({ + messageId: msg.id, + messageRole: msg.role, + text: part.text, + textLength: part.text?.length || 0, + encryptedContentLength, + time: part.time, + hasMetadata: !!part.metadata, + metadataKeys: part.metadata ? Object.keys(part.metadata) : [] + }) + } + } + } + + return reasoningBlocks + } + + async saveWrappedContext(sessionID: string, messages: any[], metadata: any, sessionMessages?: any[]) { if (!this.enabled) return try { @@ -197,11 +232,24 @@ export class Logger { } } } else { + // Extract reasoning blocks from session messages if available + const reasoningBlocks = sessionMessages + ? this.extractReasoningBlocks(sessionMessages) + : [] + content = { timestamp: new Date().toISOString(), sessionID, metadata, - messages + messages, + ...(reasoningBlocks.length > 0 && { + reasoning: { + count: reasoningBlocks.length, + totalTextCharacters: reasoningBlocks.reduce((sum, b) => sum + b.textLength, 0), + totalEncryptedCharacters: reasoningBlocks.reduce((sum, b) => sum + b.encryptedContentLength, 0), + blocks: reasoningBlocks + } + }) } } diff --git a/lib/model-selector.ts b/lib/model-selector.ts index 5051420..e0e9895 100644 --- a/lib/model-selector.ts +++ b/lib/model-selector.ts @@ -8,7 +8,7 @@ export interface ModelInfo { export const FALLBACK_MODELS: Record = { openai: 'gpt-5-mini', - anthropic: 'claude-haiku-4-5', + anthropic: 'claude-haiku-4-5', //This model isn't broken in opencode-auth-provider google: 'gemini-2.5-flash', deepseek: 'deepseek-chat', xai: 'grok-4-fast', @@ -28,6 +28,7 @@ const PROVIDER_PRIORITY = [ 'opencode' ]; +// TODO: some anthropic provided models aren't supported by the opencode-auth-provider package, so this provides a temporary workaround const SKIP_PROVIDERS = ['github-copilot', 'anthropic']; export interface ModelSelectionResult { diff --git a/lib/prompt.ts b/lib/prompt.ts index 7dc9306..9dffda7 100644 --- a/lib/prompt.ts +++ b/lib/prompt.ts @@ -26,6 +26,28 @@ function minimizeMessages(messages: any[], alreadyPrunedIds?: string[], protecte } } + // TODO: This should use the opencode normalized system instead of per provider settings + if (part.type === 'reasoning') { + // Calculate encrypted content size if present + let encryptedContentLength = 0 + if (part.metadata?.openai?.reasoningEncryptedContent) { + encryptedContentLength = part.metadata.openai.reasoningEncryptedContent.length + } else if (part.metadata?.anthropic?.signature) { + encryptedContentLength = part.metadata.anthropic.signature.length + } else if (part.metadata?.google?.thoughtSignature) { + encryptedContentLength = part.metadata.google.thoughtSignature.length + } + + return { + type: 'reasoning', + text: part.text, + textLength: part.text?.length || 0, + encryptedContentLength, + ...(part.time && { time: part.time }), + ...(part.metadata && { metadataKeys: Object.keys(part.metadata) }) + } + } + if (part.type === 'tool') { const callIDLower = part.callID?.toLowerCase() const isAlreadyPruned = prunedIdsSet.has(callIDLower) From 8bc7101ed55ede5ab9c9c0ae0d2bc625d19935a1 Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Thu, 27 Nov 2025 21:03:26 -0500 Subject: [PATCH 2/2] v0.3.20 - Bump version --- README.md | 6 +++--- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6566672..9250f78 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Add to your OpenCode config: ```jsonc // opencode.jsonc { - "plugin": ["@tarquinen/opencode-dcp@0.3.19"] + "plugin": ["@tarquinen/opencode-dcp@0.3.20"] } ``` @@ -48,7 +48,7 @@ DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.j | `pruning_summary` | `"detailed"` | `"off"`, `"minimal"`, or `"detailed"` | | `protectedTools` | `["task", "todowrite", "todoread", "context_pruning"]` | Tools that are never pruned | | `strategies.onIdle` | `["deduplication", "ai-analysis"]` | Strategies for automatic pruning | -| `strategies.onTool` | `["deduplication"]` | Strategies when AI calls `context_pruning` | +| `strategies.onTool` | `["deduplication", "ai-analysis"]` | Strategies when AI calls `context_pruning` | **Strategies:** `"deduplication"` (fast, zero LLM cost) and `"ai-analysis"` (maximum savings). Empty array disables that trigger. @@ -57,7 +57,7 @@ DCP uses its own config file (`~/.config/opencode/dcp.jsonc` or `.opencode/dcp.j "enabled": true, "strategies": { "onIdle": ["deduplication", "ai-analysis"], - "onTool": ["deduplication"] + "onTool": ["deduplication", "ai-analysis"] }, "protectedTools": ["task", "todowrite", "todoread", "context_pruning"] } diff --git a/package-lock.json b/package-lock.json index a1128a3..2e92bc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@tarquinen/opencode-dcp", - "version": "0.3.19", + "version": "0.3.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@tarquinen/opencode-dcp", - "version": "0.3.19", + "version": "0.3.20", "license": "MIT", "dependencies": { "@ai-sdk/openai-compatible": "^1.0.27", diff --git a/package.json b/package.json index e7bdeff..f38e7b0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@tarquinen/opencode-dcp", - "version": "0.3.19", + "version": "0.3.20", "type": "module", "description": "OpenCode plugin that optimizes token usage by pruning obsolete tool outputs from conversation context", "main": "./dist/index.js",