From 400fd15321cf126dcab481aaf6399805c3184751 Mon Sep 17 00:00:00 2001 From: cau1k Date: Fri, 19 Dec 2025 13:20:50 -0500 Subject: [PATCH 1/2] feat: initial impl --- index.ts | 8 +- lib/config.ts | 195 ++++++++++++++++++++++++++++++++++- lib/hooks.ts | 87 +++++++++++++++- lib/state/state.ts | 14 ++- lib/state/types.ts | 9 ++ lib/strategies/prune-tool.ts | 11 ++ package-lock.json | 15 ++- 7 files changed, 324 insertions(+), 15 deletions(-) diff --git a/index.ts b/index.ts index ac87705..b1e395a 100644 --- a/index.ts +++ b/index.ts @@ -4,7 +4,7 @@ import { Logger } from "./lib/logger" import { loadPrompt } from "./lib/prompt" import { createSessionState } from "./lib/state" import { createPruneTool } from "./lib/strategies" -import { createChatMessageTransformHandler, createEventHandler } from "./lib/hooks" +import { createChatMessageTransformHandler, createChatParamsHandler, createEventHandler } from "./lib/hooks" const plugin: Plugin = (async (ctx) => { const config = getConfig(ctx) @@ -25,10 +25,16 @@ const plugin: Plugin = (async (ctx) => { // Log initialization logger.info("DCP initialized", { strategies: config.strategies, + overrides: config.overrides }) return { + "chat.params": createChatParamsHandler(ctx.client, state, logger, config), "experimental.chat.system.transform": async (_input: unknown, output: { system: string[] }) => { + // Skip system prompt injection if DCP is disabled for current provider + if (state.provider.effectiveConfig && !state.provider.effectiveConfig.enabled) { + return + } const syntheticPrompt = loadPrompt("prune-system-prompt") output.system.push(syntheticPrompt) }, diff --git a/lib/config.ts b/lib/config.ts index 9a670fe..fe58c0b 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -38,10 +38,24 @@ export interface SupersedeWrites { enabled: boolean } +export interface ProviderStrategies { + deduplication?: { enabled?: boolean } + onIdle?: { enabled?: boolean } + pruneTool?: { enabled?: boolean } + supersedeWrites?: { enabled?: boolean } +} + +export interface ProviderOverride { + enabled?: boolean + strategies?: ProviderStrategies +} + export interface PluginConfig { enabled: boolean debug: boolean pruningSummary: "off" | "minimal" | "detailed" + disabledProviders: string[] + providerOverrides: Record strategies: { deduplication: Deduplication onIdle: OnIdle @@ -53,12 +67,15 @@ export interface PluginConfig { const DEFAULT_PROTECTED_TOOLS = ['task', 'todowrite', 'todoread', 'prune', 'batch'] // Valid config keys for validation against user config +// Note: providerOverrides uses dynamic keys so we validate structure, not exact paths export const VALID_CONFIG_KEYS = new Set([ // Top-level keys 'enabled', 'debug', 'showUpdateToasts', // Deprecated but kept for backwards compatibility 'pruningSummary', + 'disabledProviders', + 'providerOverrides', 'strategies', // strategies.deduplication 'strategies.deduplication', @@ -86,11 +103,31 @@ export const VALID_CONFIG_KEYS = new Set([ 'strategies.pruneTool.nudge.frequency' ]) +// Valid keys within providerOverrides. objects +const VALID_PROVIDER_OVERRIDE_KEYS = new Set([ + 'enabled', + 'strategies', + 'strategies.deduplication', + 'strategies.deduplication.enabled', + 'strategies.onIdle', + 'strategies.onIdle.enabled', + 'strategies.pruneTool', + 'strategies.pruneTool.enabled', + 'strategies.supersedeWrites', + 'strategies.supersedeWrites.enabled' +]) + // Extract all key paths from a config object for validation +// Skips providerOverrides children since they have dynamic provider keys function getConfigKeyPaths(obj: Record, prefix = ''): string[] { const keys: string[] = [] for (const key of Object.keys(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key + // Skip deep traversal of providerOverrides - validated separately + if (fullKey === 'providerOverrides') { + keys.push(fullKey) + continue + } keys.push(fullKey) if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) { keys.push(...getConfigKeyPaths(obj[key], fullKey)) @@ -99,10 +136,30 @@ function getConfigKeyPaths(obj: Record, prefix = ''): string[] { return keys } +// Validate providerOverrides structure +function getInvalidProviderOverrideKeys(providerOverrides: Record): string[] { + const invalid: string[] = [] + for (const provider of Object.keys(providerOverrides)) { + const override = providerOverrides[provider] + if (!override || typeof override !== 'object') continue + const overrideKeys = getConfigKeyPaths(override) + for (const key of overrideKeys) { + if (!VALID_PROVIDER_OVERRIDE_KEYS.has(key)) { + invalid.push(`providerOverrides.${provider}.${key}`) + } + } + } + return invalid +} + // Returns invalid keys found in user config export function getInvalidConfigKeys(userConfig: Record): string[] { const userKeys = getConfigKeyPaths(userConfig) - return userKeys.filter(key => !VALID_CONFIG_KEYS.has(key)) + const invalidTopLevel = userKeys.filter(key => !VALID_CONFIG_KEYS.has(key)) + const invalidProviderOverrides = userConfig.providerOverrides + ? getInvalidProviderOverrideKeys(userConfig.providerOverrides) + : [] + return [...invalidTopLevel, ...invalidProviderOverrides] } // Type validators for config values @@ -128,6 +185,37 @@ function validateConfigTypes(config: Record): ValidationError[] { errors.push({ key: 'pruningSummary', expected: '"off" | "minimal" | "detailed"', actual: JSON.stringify(config.pruningSummary) }) } } + if (config.disabledProviders !== undefined) { + if (!Array.isArray(config.disabledProviders)) { + errors.push({ key: 'disabledProviders', expected: 'string[]', actual: typeof config.disabledProviders }) + } else if (config.disabledProviders.some((p: any) => typeof p !== 'string')) { + errors.push({ key: 'disabledProviders', expected: 'string[]', actual: 'array with non-string elements' }) + } + } + if (config.providerOverrides !== undefined) { + if (typeof config.providerOverrides !== 'object' || Array.isArray(config.providerOverrides)) { + errors.push({ key: 'providerOverrides', expected: 'Record', actual: typeof config.providerOverrides }) + } else { + // Validate each provider override + for (const [provider, override] of Object.entries(config.providerOverrides)) { + if (typeof override !== 'object' || override === null) { + errors.push({ key: `providerOverrides.${provider}`, expected: 'ProviderOverride object', actual: typeof override }) + continue + } + const o = override as Record + if (o.enabled !== undefined && typeof o.enabled !== 'boolean') { + errors.push({ key: `providerOverrides.${provider}.enabled`, expected: 'boolean', actual: typeof o.enabled }) + } + if (o.strategies) { + for (const strategy of ['deduplication', 'onIdle', 'pruneTool', 'supersedeWrites']) { + if (o.strategies[strategy]?.enabled !== undefined && typeof o.strategies[strategy].enabled !== 'boolean') { + errors.push({ key: `providerOverrides.${provider}.strategies.${strategy}.enabled`, expected: 'boolean', actual: typeof o.strategies[strategy].enabled }) + } + } + } + } + } + } // Strategies validators const strategies = config.strategies @@ -246,6 +334,8 @@ const defaultConfig: PluginConfig = { enabled: true, debug: false, pruningSummary: 'detailed', + disabledProviders: [], + providerOverrides: {}, strategies: { deduplication: { enabled: true, @@ -345,6 +435,19 @@ function createDefaultConfig(): void { "debug": false, // Summary display: "off", "minimal", or "detailed" "pruningSummary": "detailed", + // Completely disable DCP for specific providers (e.g., ["openai", "gemini"]) + "disabledProviders": [], + // Per-provider overrides for granular control + // "providerOverrides": { + // "openai": { + // "enabled": false // Disable all DCP features for OpenAI + // }, + // "anthropic": { + // "strategies": { + // "pruneTool": { "enabled": false } // Disable only the prune tool for Anthropic + // } + // } + // }, // Strategies for pruning tokens from chat history "strategies": { // Remove duplicate tool calls (same tool with same arguments) @@ -470,6 +573,8 @@ function mergeStrategies( function deepCloneConfig(config: PluginConfig): PluginConfig { return { ...config, + disabledProviders: [...config.disabledProviders], + providerOverrides: JSON.parse(JSON.stringify(config.providerOverrides)), strategies: { deduplication: { ...config.strategies.deduplication, @@ -492,6 +597,30 @@ function deepCloneConfig(config: PluginConfig): PluginConfig { } } +function mergeProviderOverrides( + base: Record, + override?: Record +): Record { + if (!override) return base + const result = { ...base } + for (const [provider, overrideConfig] of Object.entries(override)) { + if (!result[provider]) { + result[provider] = overrideConfig + } else { + result[provider] = { + enabled: overrideConfig.enabled ?? result[provider].enabled, + strategies: overrideConfig.strategies + ? { + ...result[provider].strategies, + ...overrideConfig.strategies + } + : result[provider].strategies + } + } + } + return result +} + export function getConfig(ctx: PluginInput): PluginConfig { let config = deepCloneConfig(defaultConfig) @@ -520,6 +649,8 @@ export function getConfig(ctx: PluginInput): PluginConfig { enabled: result.data.enabled ?? config.enabled, debug: result.data.debug ?? config.debug, pruningSummary: result.data.pruningSummary ?? config.pruningSummary, + disabledProviders: result.data.disabledProviders ?? config.disabledProviders, + providerOverrides: mergeProviderOverrides(config.providerOverrides, result.data.providerOverrides), strategies: mergeStrategies(config.strategies, result.data.strategies as any) } } @@ -551,6 +682,8 @@ export function getConfig(ctx: PluginInput): PluginConfig { enabled: result.data.enabled ?? config.enabled, debug: result.data.debug ?? config.debug, pruningSummary: result.data.pruningSummary ?? config.pruningSummary, + disabledProviders: result.data.disabledProviders ?? config.disabledProviders, + providerOverrides: mergeProviderOverrides(config.providerOverrides, result.data.providerOverrides), strategies: mergeStrategies(config.strategies, result.data.strategies as any) } } @@ -579,6 +712,8 @@ export function getConfig(ctx: PluginInput): PluginConfig { enabled: result.data.enabled ?? config.enabled, debug: result.data.debug ?? config.debug, pruningSummary: result.data.pruningSummary ?? config.pruningSummary, + disabledProviders: result.data.disabledProviders ?? config.disabledProviders, + providerOverrides: mergeProviderOverrides(config.providerOverrides, result.data.providerOverrides), strategies: mergeStrategies(config.strategies, result.data.strategies as any) } } @@ -586,3 +721,61 @@ export function getConfig(ctx: PluginInput): PluginConfig { return config } + +export interface EffectiveProviderConfig { + enabled: boolean + strategies: { + deduplication: boolean + onIdle: boolean + pruneTool: boolean + supersedeWrites: boolean + } +} + +/** + * Computes the effective config for a specific provider, applying: + * 1. disabledProviders list (completely disables all features) + * 2. providerOverrides (granular per-strategy overrides) + */ +export function getEffectiveProviderConfig( + config: PluginConfig, + providerID: string +): EffectiveProviderConfig { + // Check if provider is completely disabled + if (config.disabledProviders.includes(providerID)) { + return { + enabled: false, + strategies: { + deduplication: false, + onIdle: false, + pruneTool: false, + supersedeWrites: false + } + } + } + + // Check for provider-specific overrides + const override = config.providerOverrides[providerID] + if (override?.enabled === false) { + return { + enabled: false, + strategies: { + deduplication: false, + onIdle: false, + pruneTool: false, + supersedeWrites: false + } + } + } + + // Apply granular strategy overrides + return { + enabled: true, + strategies: { + deduplication: override?.strategies?.deduplication?.enabled ?? config.strategies.deduplication.enabled, + onIdle: override?.strategies?.onIdle?.enabled ?? config.strategies.onIdle.enabled, + pruneTool: override?.strategies?.pruneTool?.enabled ?? config.strategies.pruneTool.enabled, + supersedeWrites: override?.strategies?.supersedeWrites?.enabled ?? config.strategies.supersedeWrites.enabled + } + } +} diff --git a/lib/hooks.ts b/lib/hooks.ts index c578e4b..426fc8c 100644 --- a/lib/hooks.ts +++ b/lib/hooks.ts @@ -1,12 +1,64 @@ import type { SessionState, WithParts } from "./state" import type { Logger } from "./logger" import type { PluginConfig } from "./config" +import { getEffectiveProviderConfig } from "./config" import { syncToolCache } from "./state/tool-cache" import { deduplicate, supersedeWrites } from "./strategies" import { prune, insertPruneToolContext } from "./messages" import { checkSession } from "./state" import { runOnIdle } from "./strategies/on-idle" +export function createChatParamsHandler( + client: any, + state: SessionState, + logger: Logger, + config: PluginConfig +) { + return async ( + input: { + sessionID: string + agent: string + model: { id: string; providerID: string; name: string } + provider: { source: string; info: any; options: Record } + message: any + }, + _output: { temperature: number; topP: number; options: Record } + ) => { + const { model } = input + const providerID = model.providerID + + // Check if provider changed + if (state.provider.providerID !== providerID) { + logger.info("Provider changed", { + from: state.provider.providerID, + to: providerID, + model: model.id + }) + + state.provider.providerID = providerID + state.provider.modelID = model.id + state.provider.effectiveConfig = getEffectiveProviderConfig(config, providerID) + + // Show toast if DCP is disabled for this provider (but only once per provider switch) + if (!state.provider.effectiveConfig.enabled && state.provider.lastNotifiedProvider !== providerID) { + state.provider.lastNotifiedProvider = providerID + try { + await client.tui.showToast({ + body: { + title: "DCP Disabled", + message: `Dynamic Context Pruning is disabled for ${providerID}`, + variant: "info", + duration: 4000 + } + }) + } catch { + // Ignore toast errors + } + } + } + } +} + export function createChatMessageTransformHandler( client: any, @@ -24,14 +76,35 @@ export function createChatMessageTransformHandler( return } + // Check if DCP is disabled for the current provider + const effectiveConfig = state.provider.effectiveConfig + if (effectiveConfig && !effectiveConfig.enabled) { + logger.info("DCP disabled for provider", { provider: state.provider.providerID }) + return + } + syncToolCache(state, config, logger, output.messages); - deduplicate(state, logger, config, output.messages) - supersedeWrites(state, logger, config, output.messages) + // Apply strategies based on effective config (respects provider overrides) + const strategies = effectiveConfig?.strategies ?? { + deduplication: config.strategies.deduplication.enabled, + onIdle: config.strategies.onIdle.enabled, + pruneTool: config.strategies.pruneTool.enabled, + supersedeWrites: config.strategies.supersedeWrites.enabled + } + + if (strategies.deduplication) { + deduplicate(state, logger, config, output.messages) + } + if (strategies.supersedeWrites) { + supersedeWrites(state, logger, config, output.messages) + } prune(state, logger, config, output.messages) - insertPruneToolContext(state, config, logger, output.messages) + if (strategies.pruneTool) { + insertPruneToolContext(state, config, logger, output.messages) + } } } @@ -50,6 +123,14 @@ export function createEventHandler( } if (event.type === "session.status" && event.properties.status.type === "idle") { + // Check if onIdle is disabled globally or for current provider + const effectiveConfig = state.provider.effectiveConfig + if (effectiveConfig && !effectiveConfig.enabled) { + return + } + if (effectiveConfig && !effectiveConfig.strategies.onIdle) { + return + } if (!config.strategies.onIdle.enabled) { return } diff --git a/lib/state/state.ts b/lib/state/state.ts index e33f4c8..e1464db 100644 --- a/lib/state/state.ts +++ b/lib/state/state.ts @@ -53,7 +53,13 @@ export function createSessionState(): SessionState { nudgeCounter: 0, lastToolPrune: false, lastCompaction: 0, - currentTurn: 0 + currentTurn: 0, + provider: { + providerID: null, + modelID: null, + effectiveConfig: null, + lastNotifiedProvider: null + } } } @@ -72,6 +78,12 @@ export function resetSessionState(state: SessionState): void { state.lastToolPrune = false state.lastCompaction = 0 state.currentTurn = 0 + state.provider = { + providerID: null, + modelID: null, + effectiveConfig: null, + lastNotifiedProvider: null + } } export async function ensureSessionInitialized( diff --git a/lib/state/types.ts b/lib/state/types.ts index 04847d5..a22e782 100644 --- a/lib/state/types.ts +++ b/lib/state/types.ts @@ -1,4 +1,5 @@ import { Message, Part } from "@opencode-ai/sdk" +import type { EffectiveProviderConfig } from "../config" export interface WithParts { info: Message @@ -24,6 +25,13 @@ export interface Prune { toolIds: string[] } +export interface ProviderState { + providerID: string | null + modelID: string | null + effectiveConfig: EffectiveProviderConfig | null + lastNotifiedProvider: string | null +} + export interface SessionState { sessionId: string | null isSubAgent: boolean @@ -34,4 +42,5 @@ export interface SessionState { lastToolPrune: boolean lastCompaction: number currentTurn: number // Current turn count derived from step-start parts + provider: ProviderState } diff --git a/lib/strategies/prune-tool.ts b/lib/strategies/prune-tool.ts index 285a88d..772afcc 100644 --- a/lib/strategies/prune-tool.ts +++ b/lib/strategies/prune-tool.ts @@ -50,6 +50,17 @@ export function createPruneTool( logger.info("Prune tool invoked") logger.info(JSON.stringify(args)) + // Check if prune tool is disabled for current provider + const effectiveConfig = state.provider.effectiveConfig + if (effectiveConfig && !effectiveConfig.enabled) { + logger.info("Prune tool disabled for provider", { provider: state.provider.providerID }) + return `DCP is disabled for ${state.provider.providerID}. Pruning is not available.` + } + if (effectiveConfig && !effectiveConfig.strategies.pruneTool) { + logger.info("Prune tool strategy disabled for provider", { provider: state.provider.providerID }) + return `Prune tool is disabled for ${state.provider.providerID}. Pruning is not available.` + } + if (!args.ids || args.ids.length === 0) { logger.debug("Prune tool called but args.ids is empty or undefined: " + JSON.stringify(args)) return "No IDs provided. Check the list for available IDs to prune." diff --git a/package-lock.json b/package-lock.json index 42b0ebd..00b6960 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "resolved": "https://registry.npmjs.org/@ai-sdk/openai-compatible/-/openai-compatible-1.0.28.tgz", "integrity": "sha512-yKubDxLYtXyGUzkr9lNStf/lE/I+Okc8tmotvyABhsQHHieLKk6oV5fJeRJxhr67Ejhg+FRnwUOxAmjRoFM4dA==", "license": "Apache-2.0", + "peer": true, "dependencies": { "@ai-sdk/provider": "2.0.0", "@ai-sdk/provider-utils": "3.0.18" @@ -1289,7 +1290,6 @@ "resolved": "https://registry.npmjs.org/@oslojs/asn1/-/asn1-1.0.0.tgz", "integrity": "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==", "license": "MIT", - "peer": true, "dependencies": { "@oslojs/binary": "1.0.0" } @@ -1298,15 +1298,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@oslojs/binary/-/binary-1.0.0.tgz", "integrity": "sha512-9RCU6OwXU6p67H4NODbuxv2S3eenuQ4/WFLrsq+K/k682xrznH5EVWA7N4VFk9VYVcbFtKqur5YQQZc0ySGhsQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@oslojs/crypto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@oslojs/crypto/-/crypto-1.0.1.tgz", "integrity": "sha512-7n08G8nWjAr/Yu3vu9zzrd0L9XnrJfpMioQcvCMxBIiF5orECHe5/3J0jmXRVvgfqMm/+4oxlQ+Sq39COYLcNQ==", "license": "MIT", - "peer": true, "dependencies": { "@oslojs/asn1": "1.0.0", "@oslojs/binary": "1.0.0" @@ -1316,15 +1314,13 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@oslojs/jwt": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@oslojs/jwt/-/jwt-0.2.0.tgz", "integrity": "sha512-bLE7BtHrURedCn4Mco3ma9L4Y1GR2SMBuIvjWr7rmQ4/W/4Jy70TIAgZ+0nIlk0xHz1vNP8x8DCns45Sb2XRbg==", "license": "MIT", - "peer": true, "dependencies": { "@oslojs/encoding": "0.4.1" } @@ -1333,8 +1329,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-0.4.1.tgz", "integrity": "sha512-hkjo6MuIK/kQR5CrGNdAPZhS01ZCXuWDRJ187zh6qqF2+yMHZpD9fAYpX8q2bOO6Ryhl3XpCT6kUX76N8hhm4Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/@smithy/abort-controller": { "version": "4.2.5", @@ -2269,6 +2264,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -2301,6 +2297,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } From a9431cd50619f532e459f6f5fe2f711e2c5bb799 Mon Sep 17 00:00:00 2001 From: cau1k Date: Fri, 19 Dec 2025 13:30:21 -0500 Subject: [PATCH 2/2] refac: consolidate config into overrides.provider. --- lib/config.ts | 165 ++++++++++++++++++++++---------------------------- 1 file changed, 74 insertions(+), 91 deletions(-) diff --git a/lib/config.ts b/lib/config.ts index fe58c0b..533ac4d 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -50,12 +50,15 @@ export interface ProviderOverride { strategies?: ProviderStrategies } +export interface Overrides { + provider?: Record +} + export interface PluginConfig { enabled: boolean debug: boolean pruningSummary: "off" | "minimal" | "detailed" - disabledProviders: string[] - providerOverrides: Record + overrides: Overrides strategies: { deduplication: Deduplication onIdle: OnIdle @@ -67,15 +70,15 @@ export interface PluginConfig { const DEFAULT_PROTECTED_TOOLS = ['task', 'todowrite', 'todoread', 'prune', 'batch'] // Valid config keys for validation against user config -// Note: providerOverrides uses dynamic keys so we validate structure, not exact paths +// Note: overrides.provider uses dynamic keys so we validate structure, not exact paths export const VALID_CONFIG_KEYS = new Set([ // Top-level keys 'enabled', 'debug', 'showUpdateToasts', // Deprecated but kept for backwards compatibility 'pruningSummary', - 'disabledProviders', - 'providerOverrides', + 'overrides', + 'overrides.provider', 'strategies', // strategies.deduplication 'strategies.deduplication', @@ -103,7 +106,7 @@ export const VALID_CONFIG_KEYS = new Set([ 'strategies.pruneTool.nudge.frequency' ]) -// Valid keys within providerOverrides. objects +// Valid keys within overrides.provider. objects const VALID_PROVIDER_OVERRIDE_KEYS = new Set([ 'enabled', 'strategies', @@ -118,13 +121,13 @@ const VALID_PROVIDER_OVERRIDE_KEYS = new Set([ ]) // Extract all key paths from a config object for validation -// Skips providerOverrides children since they have dynamic provider keys +// Skips overrides.provider children since they have dynamic provider keys function getConfigKeyPaths(obj: Record, prefix = ''): string[] { const keys: string[] = [] for (const key of Object.keys(obj)) { const fullKey = prefix ? `${prefix}.${key}` : key - // Skip deep traversal of providerOverrides - validated separately - if (fullKey === 'providerOverrides') { + // Skip deep traversal of overrides.provider - validated separately + if (fullKey === 'overrides.provider') { keys.push(fullKey) continue } @@ -136,7 +139,7 @@ function getConfigKeyPaths(obj: Record, prefix = ''): string[] { return keys } -// Validate providerOverrides structure +// Validate overrides.provider structure function getInvalidProviderOverrideKeys(providerOverrides: Record): string[] { const invalid: string[] = [] for (const provider of Object.keys(providerOverrides)) { @@ -145,7 +148,7 @@ function getInvalidProviderOverrideKeys(providerOverrides: Record): const overrideKeys = getConfigKeyPaths(override) for (const key of overrideKeys) { if (!VALID_PROVIDER_OVERRIDE_KEYS.has(key)) { - invalid.push(`providerOverrides.${provider}.${key}`) + invalid.push(`overrides.provider.${provider}.${key}`) } } } @@ -156,8 +159,8 @@ function getInvalidProviderOverrideKeys(providerOverrides: Record): export function getInvalidConfigKeys(userConfig: Record): string[] { const userKeys = getConfigKeyPaths(userConfig) const invalidTopLevel = userKeys.filter(key => !VALID_CONFIG_KEYS.has(key)) - const invalidProviderOverrides = userConfig.providerOverrides - ? getInvalidProviderOverrideKeys(userConfig.providerOverrides) + const invalidProviderOverrides = userConfig.overrides?.provider + ? getInvalidProviderOverrideKeys(userConfig.overrides.provider) : [] return [...invalidTopLevel, ...invalidProviderOverrides] } @@ -185,31 +188,28 @@ function validateConfigTypes(config: Record): ValidationError[] { errors.push({ key: 'pruningSummary', expected: '"off" | "minimal" | "detailed"', actual: JSON.stringify(config.pruningSummary) }) } } - if (config.disabledProviders !== undefined) { - if (!Array.isArray(config.disabledProviders)) { - errors.push({ key: 'disabledProviders', expected: 'string[]', actual: typeof config.disabledProviders }) - } else if (config.disabledProviders.some((p: any) => typeof p !== 'string')) { - errors.push({ key: 'disabledProviders', expected: 'string[]', actual: 'array with non-string elements' }) - } - } - if (config.providerOverrides !== undefined) { - if (typeof config.providerOverrides !== 'object' || Array.isArray(config.providerOverrides)) { - errors.push({ key: 'providerOverrides', expected: 'Record', actual: typeof config.providerOverrides }) - } else { - // Validate each provider override - for (const [provider, override] of Object.entries(config.providerOverrides)) { - if (typeof override !== 'object' || override === null) { - errors.push({ key: `providerOverrides.${provider}`, expected: 'ProviderOverride object', actual: typeof override }) - continue - } - const o = override as Record - if (o.enabled !== undefined && typeof o.enabled !== 'boolean') { - errors.push({ key: `providerOverrides.${provider}.enabled`, expected: 'boolean', actual: typeof o.enabled }) - } - if (o.strategies) { - for (const strategy of ['deduplication', 'onIdle', 'pruneTool', 'supersedeWrites']) { - if (o.strategies[strategy]?.enabled !== undefined && typeof o.strategies[strategy].enabled !== 'boolean') { - errors.push({ key: `providerOverrides.${provider}.strategies.${strategy}.enabled`, expected: 'boolean', actual: typeof o.strategies[strategy].enabled }) + if (config.overrides !== undefined) { + if (typeof config.overrides !== 'object' || Array.isArray(config.overrides)) { + errors.push({ key: 'overrides', expected: 'Overrides object', actual: typeof config.overrides }) + } else if (config.overrides.provider !== undefined) { + if (typeof config.overrides.provider !== 'object' || Array.isArray(config.overrides.provider)) { + errors.push({ key: 'overrides.provider', expected: 'Record', actual: typeof config.overrides.provider }) + } else { + // Validate each provider override + for (const [provider, override] of Object.entries(config.overrides.provider)) { + if (typeof override !== 'object' || override === null) { + errors.push({ key: `overrides.provider.${provider}`, expected: 'ProviderOverride object', actual: typeof override }) + continue + } + const o = override as Record + if (o.enabled !== undefined && typeof o.enabled !== 'boolean') { + errors.push({ key: `overrides.provider.${provider}.enabled`, expected: 'boolean', actual: typeof o.enabled }) + } + if (o.strategies) { + for (const strategy of ['deduplication', 'onIdle', 'pruneTool', 'supersedeWrites']) { + if (o.strategies[strategy]?.enabled !== undefined && typeof o.strategies[strategy].enabled !== 'boolean') { + errors.push({ key: `overrides.provider.${provider}.strategies.${strategy}.enabled`, expected: 'boolean', actual: typeof o.strategies[strategy].enabled }) + } } } } @@ -334,8 +334,7 @@ const defaultConfig: PluginConfig = { enabled: true, debug: false, pruningSummary: 'detailed', - disabledProviders: [], - providerOverrides: {}, + overrides: {}, strategies: { deduplication: { enabled: true, @@ -435,16 +434,16 @@ function createDefaultConfig(): void { "debug": false, // Summary display: "off", "minimal", or "detailed" "pruningSummary": "detailed", - // Completely disable DCP for specific providers (e.g., ["openai", "gemini"]) - "disabledProviders": [], // Per-provider overrides for granular control - // "providerOverrides": { - // "openai": { - // "enabled": false // Disable all DCP features for OpenAI - // }, - // "anthropic": { - // "strategies": { - // "pruneTool": { "enabled": false } // Disable only the prune tool for Anthropic + // "overrides": { + // "provider": { + // "openai": { + // "enabled": false // Disable all DCP features for OpenAI + // }, + // "anthropic": { + // "strategies": { + // "pruneTool": { "enabled": false } // Disable only the prune tool for Anthropic + // } // } // } // }, @@ -573,8 +572,7 @@ function mergeStrategies( function deepCloneConfig(config: PluginConfig): PluginConfig { return { ...config, - disabledProviders: [...config.disabledProviders], - providerOverrides: JSON.parse(JSON.stringify(config.providerOverrides)), + overrides: JSON.parse(JSON.stringify(config.overrides)), strategies: { deduplication: { ...config.strategies.deduplication, @@ -597,24 +595,27 @@ function deepCloneConfig(config: PluginConfig): PluginConfig { } } -function mergeProviderOverrides( - base: Record, - override?: Record -): Record { +function mergeOverrides( + base: Overrides, + override?: Overrides +): Overrides { if (!override) return base - const result = { ...base } - for (const [provider, overrideConfig] of Object.entries(override)) { - if (!result[provider]) { - result[provider] = overrideConfig - } else { - result[provider] = { - enabled: overrideConfig.enabled ?? result[provider].enabled, - strategies: overrideConfig.strategies - ? { - ...result[provider].strategies, - ...overrideConfig.strategies - } - : result[provider].strategies + const result: Overrides = { ...base } + if (override.provider) { + result.provider = result.provider ?? {} + for (const [provider, overrideConfig] of Object.entries(override.provider)) { + if (!result.provider[provider]) { + result.provider[provider] = overrideConfig + } else { + result.provider[provider] = { + enabled: overrideConfig.enabled ?? result.provider[provider].enabled, + strategies: overrideConfig.strategies + ? { + ...result.provider[provider].strategies, + ...overrideConfig.strategies + } + : result.provider[provider].strategies + } } } } @@ -649,8 +650,7 @@ export function getConfig(ctx: PluginInput): PluginConfig { enabled: result.data.enabled ?? config.enabled, debug: result.data.debug ?? config.debug, pruningSummary: result.data.pruningSummary ?? config.pruningSummary, - disabledProviders: result.data.disabledProviders ?? config.disabledProviders, - providerOverrides: mergeProviderOverrides(config.providerOverrides, result.data.providerOverrides), + overrides: mergeOverrides(config.overrides, result.data.overrides), strategies: mergeStrategies(config.strategies, result.data.strategies as any) } } @@ -682,8 +682,7 @@ export function getConfig(ctx: PluginInput): PluginConfig { enabled: result.data.enabled ?? config.enabled, debug: result.data.debug ?? config.debug, pruningSummary: result.data.pruningSummary ?? config.pruningSummary, - disabledProviders: result.data.disabledProviders ?? config.disabledProviders, - providerOverrides: mergeProviderOverrides(config.providerOverrides, result.data.providerOverrides), + overrides: mergeOverrides(config.overrides, result.data.overrides), strategies: mergeStrategies(config.strategies, result.data.strategies as any) } } @@ -712,8 +711,7 @@ export function getConfig(ctx: PluginInput): PluginConfig { enabled: result.data.enabled ?? config.enabled, debug: result.data.debug ?? config.debug, pruningSummary: result.data.pruningSummary ?? config.pruningSummary, - disabledProviders: result.data.disabledProviders ?? config.disabledProviders, - providerOverrides: mergeProviderOverrides(config.providerOverrides, result.data.providerOverrides), + overrides: mergeOverrides(config.overrides, result.data.overrides), strategies: mergeStrategies(config.strategies, result.data.strategies as any) } } @@ -733,29 +731,14 @@ export interface EffectiveProviderConfig { } /** - * Computes the effective config for a specific provider, applying: - * 1. disabledProviders list (completely disables all features) - * 2. providerOverrides (granular per-strategy overrides) + * Computes the effective config for a specific provider, applying overrides.provider. */ export function getEffectiveProviderConfig( config: PluginConfig, providerID: string ): EffectiveProviderConfig { - // Check if provider is completely disabled - if (config.disabledProviders.includes(providerID)) { - return { - enabled: false, - strategies: { - deduplication: false, - onIdle: false, - pruneTool: false, - supersedeWrites: false - } - } - } - // Check for provider-specific overrides - const override = config.providerOverrides[providerID] + const override = config.overrides.provider?.[providerID] if (override?.enabled === false) { return { enabled: false,