From ad46d65d207860103e2303c8e8acf6124ddd9e61 Mon Sep 17 00:00:00 2001 From: spoons-and-mirrors <212802214+spoons-and-mirrors@users.noreply.github.com> Date: Mon, 29 Dec 2025 01:33:36 +0100 Subject: [PATCH] feat: activationThreshold --- index.ts | 13 ++++++++++--- lib/config.ts | 19 ++++++++++++++++++- lib/hooks.ts | 3 +++ lib/messages/prune.ts | 8 ++++++++ lib/shared-utils.ts | 29 +++++++++++++++++++++++++++++ lib/state/state.ts | 2 ++ lib/state/types.ts | 3 ++- 7 files changed, 72 insertions(+), 5 deletions(-) diff --git a/index.ts b/index.ts index 0c2906a..a1bffd9 100644 --- a/index.ts +++ b/index.ts @@ -33,15 +33,22 @@ const plugin: Plugin = (async (ctx) => { const discardEnabled = config.tools.discard.enabled const extractEnabled = config.tools.extract.enabled + if (!discardEnabled && !extractEnabled) { + return + } + + const threshold = config.tools.settings.activationThreshold + if (state.thresholdState < threshold) { + return + } + let promptName: string if (discardEnabled && extractEnabled) { promptName = "user/system/system-prompt-both" } else if (discardEnabled) { promptName = "user/system/system-prompt-discard" - } else if (extractEnabled) { - promptName = "user/system/system-prompt-extract" } else { - return + promptName = "user/system/system-prompt-extract" } const syntheticPrompt = loadPrompt(promptName) diff --git a/lib/config.ts b/lib/config.ts index 15fe647..c4a3d0a 100644 --- a/lib/config.ts +++ b/lib/config.ts @@ -30,6 +30,7 @@ export interface ToolSettings { nudgeEnabled: boolean nudgeFrequency: number protectedTools: string[] + activationThreshold: number } export interface Tools { @@ -84,6 +85,7 @@ export const VALID_CONFIG_KEYS = new Set([ "tools.settings.nudgeEnabled", "tools.settings.nudgeFrequency", "tools.settings.protectedTools", + "tools.settings.activationThreshold", "tools.discard", "tools.discard.enabled", "tools.extract", @@ -216,6 +218,16 @@ function validateConfigTypes(config: Record): ValidationError[] { actual: typeof tools.settings.protectedTools, }) } + if ( + tools.settings.minToolOutputTokens !== undefined && + typeof tools.settings.minToolOutputTokens !== "number" + ) { + errors.push({ + key: "tools.settings.minToolOutputTokens", + expected: "number", + actual: typeof tools.settings.minToolOutputTokens, + }) + } } if (tools.discard) { if (tools.discard.enabled !== undefined && typeof tools.discard.enabled !== "boolean") { @@ -437,6 +449,7 @@ const defaultConfig: PluginConfig = { nudgeEnabled: true, nudgeFrequency: 10, protectedTools: [...DEFAULT_PROTECTED_TOOLS], + activationThreshold: 5000, }, discard: { enabled: true, @@ -555,7 +568,9 @@ function createDefaultConfig(): void { "nudgeEnabled": true, "nudgeFrequency": 10, // Additional tools to protect from pruning - "protectedTools": [] + "protectedTools": [], + // Minimum tool output tokens before injecting prune context + "activationThreshold": 5000 }, // Removes tool content from context without preservation (for completed tasks or noise) "discard": { @@ -693,6 +708,8 @@ function mergeTools( ...(override.settings?.protectedTools ?? []), ]), ], + activationThreshold: + override.settings?.activationThreshold ?? base.settings.activationThreshold, }, discard: { enabled: override.discard?.enabled ?? base.discard.enabled, diff --git a/lib/hooks.ts b/lib/hooks.ts index d3ec6d2..83fc21a 100644 --- a/lib/hooks.ts +++ b/lib/hooks.ts @@ -6,6 +6,7 @@ import { deduplicate, supersedeWrites, purgeErrors } from "./strategies" import { prune, insertPruneToolContext } from "./messages" import { checkSession } from "./state" import { runOnIdle } from "./strategies/on-idle" +import { getThreshold } from "./shared-utils" export function createChatMessageTransformHandler( client: any, @@ -28,6 +29,8 @@ export function createChatMessageTransformHandler( prune(state, logger, config, output.messages) + state.thresholdState = getThreshold(state, output.messages) + insertPruneToolContext(state, config, logger, output.messages) if (state.sessionId) { diff --git a/lib/messages/prune.ts b/lib/messages/prune.ts index 351be09..c1c3cd9 100644 --- a/lib/messages/prune.ts +++ b/lib/messages/prune.ts @@ -102,6 +102,14 @@ export const insertPruneToolContext = ( return } + const threshold = config.tools.settings.activationThreshold + if (state.thresholdState < threshold) { + logger.debug( + `Skipping prune context injection: ${state.thresholdState} tokens < ${threshold} threshold`, + ) + return + } + let prunableToolsContent: string if (state.lastToolPrune) { diff --git a/lib/shared-utils.ts b/lib/shared-utils.ts index ce3be56..5666ed0 100644 --- a/lib/shared-utils.ts +++ b/lib/shared-utils.ts @@ -1,9 +1,38 @@ +import { encode } from "gpt-tokenizer" import { SessionState, WithParts } from "./state" export const isMessageCompacted = (state: SessionState, msg: WithParts): boolean => { return msg.info.time.created < state.lastCompaction } +export const getThreshold = (state: SessionState, messages: WithParts[]): number => { + let totalTokens = 0 + + for (const msg of messages) { + if (isMessageCompacted(state, msg)) { + continue + } + + for (const part of msg.parts) { + if (part.type !== "tool") { + continue + } + if (state.prune.toolIds.includes(part.callID)) { + continue + } + if (part.state.status === "completed") { + const output = part.state.output + if (output) { + const outputStr = typeof output === "string" ? output : JSON.stringify(output) + totalTokens += encode(outputStr).length + } + } + } + } + + return totalTokens +} + export const getLastUserMessage = (messages: WithParts[]): WithParts | null => { for (let i = messages.length - 1; i >= 0; i--) { const msg = messages[i] diff --git a/lib/state/state.ts b/lib/state/state.ts index 956ac9d..89ccbe2 100644 --- a/lib/state/state.ts +++ b/lib/state/state.ts @@ -55,6 +55,7 @@ export function createSessionState(): SessionState { lastToolPrune: false, lastCompaction: 0, currentTurn: 0, + thresholdState: 0, } } @@ -73,6 +74,7 @@ export function resetSessionState(state: SessionState): void { state.lastToolPrune = false state.lastCompaction = 0 state.currentTurn = 0 + state.thresholdState = 0 } export async function ensureSessionInitialized( diff --git a/lib/state/types.ts b/lib/state/types.ts index 9a6de02..3044cea 100644 --- a/lib/state/types.ts +++ b/lib/state/types.ts @@ -33,5 +33,6 @@ export interface SessionState { nudgeCounter: number lastToolPrune: boolean lastCompaction: number - currentTurn: number // Current turn count derived from step-start parts + currentTurn: number + thresholdState: number }