Skip to content
Draft
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
8 changes: 7 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
},
Expand Down
178 changes: 177 additions & 1 deletion lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,27 @@ 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 Overrides {
provider?: Record<string, ProviderOverride>
}

export interface PluginConfig {
enabled: boolean
debug: boolean
pruningSummary: "off" | "minimal" | "detailed"
overrides: Overrides
strategies: {
deduplication: Deduplication
onIdle: OnIdle
Expand All @@ -53,12 +70,15 @@ export interface PluginConfig {
const DEFAULT_PROTECTED_TOOLS = ['task', 'todowrite', 'todoread', 'prune', 'batch']

// Valid config keys for validation against user config
// 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',
'overrides',
'overrides.provider',
'strategies',
// strategies.deduplication
'strategies.deduplication',
Expand Down Expand Up @@ -86,11 +106,31 @@ export const VALID_CONFIG_KEYS = new Set([
'strategies.pruneTool.nudge.frequency'
])

// Valid keys within overrides.provider.<provider> 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 overrides.provider children since they have dynamic provider keys
function getConfigKeyPaths(obj: Record<string, any>, prefix = ''): string[] {
const keys: string[] = []
for (const key of Object.keys(obj)) {
const fullKey = prefix ? `${prefix}.${key}` : key
// Skip deep traversal of overrides.provider - validated separately
if (fullKey === 'overrides.provider') {
keys.push(fullKey)
continue
}
keys.push(fullKey)
if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
keys.push(...getConfigKeyPaths(obj[key], fullKey))
Expand All @@ -99,10 +139,30 @@ function getConfigKeyPaths(obj: Record<string, any>, prefix = ''): string[] {
return keys
}

// Validate overrides.provider structure
function getInvalidProviderOverrideKeys(providerOverrides: Record<string, any>): 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(`overrides.provider.${provider}.${key}`)
}
}
}
return invalid
}

// Returns invalid keys found in user config
export function getInvalidConfigKeys(userConfig: Record<string, any>): 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.overrides?.provider
? getInvalidProviderOverrideKeys(userConfig.overrides.provider)
: []
return [...invalidTopLevel, ...invalidProviderOverrides]
}

// Type validators for config values
Expand All @@ -128,6 +188,34 @@ function validateConfigTypes(config: Record<string, any>): ValidationError[] {
errors.push({ key: 'pruningSummary', expected: '"off" | "minimal" | "detailed"', actual: JSON.stringify(config.pruningSummary) })
}
}
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<string, ProviderOverride>', 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<string, any>
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 })
}
}
}
}
}
}
}

// Strategies validators
const strategies = config.strategies
Expand Down Expand Up @@ -246,6 +334,7 @@ const defaultConfig: PluginConfig = {
enabled: true,
debug: false,
pruningSummary: 'detailed',
overrides: {},
strategies: {
deduplication: {
enabled: true,
Expand Down Expand Up @@ -345,6 +434,19 @@ function createDefaultConfig(): void {
"debug": false,
// Summary display: "off", "minimal", or "detailed"
"pruningSummary": "detailed",
// Per-provider overrides for granular control
// "overrides": {
// "provider": {
// "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)
Expand Down Expand Up @@ -470,6 +572,7 @@ function mergeStrategies(
function deepCloneConfig(config: PluginConfig): PluginConfig {
return {
...config,
overrides: JSON.parse(JSON.stringify(config.overrides)),
strategies: {
deduplication: {
...config.strategies.deduplication,
Expand All @@ -492,6 +595,33 @@ function deepCloneConfig(config: PluginConfig): PluginConfig {
}
}

function mergeOverrides(
base: Overrides,
override?: Overrides
): Overrides {
if (!override) return base
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
}
}
}
}
return result
}


export function getConfig(ctx: PluginInput): PluginConfig {
let config = deepCloneConfig(defaultConfig)
Expand Down Expand Up @@ -520,6 +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,
overrides: mergeOverrides(config.overrides, result.data.overrides),
strategies: mergeStrategies(config.strategies, result.data.strategies as any)
}
}
Expand Down Expand Up @@ -551,6 +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,
overrides: mergeOverrides(config.overrides, result.data.overrides),
strategies: mergeStrategies(config.strategies, result.data.strategies as any)
}
}
Expand Down Expand Up @@ -579,10 +711,54 @@ export function getConfig(ctx: PluginInput): PluginConfig {
enabled: result.data.enabled ?? config.enabled,
debug: result.data.debug ?? config.debug,
pruningSummary: result.data.pruningSummary ?? config.pruningSummary,
overrides: mergeOverrides(config.overrides, result.data.overrides),
strategies: mergeStrategies(config.strategies, result.data.strategies as any)
}
}
}

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 overrides.provider.<name>
*/
export function getEffectiveProviderConfig(
config: PluginConfig,
providerID: string
): EffectiveProviderConfig {
// Check for provider-specific overrides
const override = config.overrides.provider?.[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
}
}
}
Loading