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
13 changes: 10 additions & 3 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 18 additions & 1 deletion lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface ToolSettings {
nudgeEnabled: boolean
nudgeFrequency: number
protectedTools: string[]
activationThreshold: number
}

export interface Tools {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -216,6 +218,16 @@ function validateConfigTypes(config: Record<string, any>): 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") {
Expand Down Expand Up @@ -437,6 +449,7 @@ const defaultConfig: PluginConfig = {
nudgeEnabled: true,
nudgeFrequency: 10,
protectedTools: [...DEFAULT_PROTECTED_TOOLS],
activationThreshold: 5000,
},
discard: {
enabled: true,
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -693,6 +708,8 @@ function mergeTools(
...(override.settings?.protectedTools ?? []),
]),
],
activationThreshold:
override.settings?.activationThreshold ?? base.settings.activationThreshold,
},
discard: {
enabled: override.discard?.enabled ?? base.discard.enabled,
Expand Down
3 changes: 3 additions & 0 deletions lib/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,6 +29,8 @@ export function createChatMessageTransformHandler(

prune(state, logger, config, output.messages)

state.thresholdState = getThreshold(state, output.messages)
Copy link
Collaborator

@Tarquinen Tarquinen Dec 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this be done within inject.ts as well?


insertPruneToolContext(state, config, logger, output.messages)

if (state.sessionId) {
Expand Down
8 changes: 8 additions & 0 deletions lib/messages/prune.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
29 changes: 29 additions & 0 deletions lib/shared-utils.ts
Original file line number Diff line number Diff line change
@@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably check for the tool type because write and edit tools have different parts pruned and add those differently

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]
Expand Down
2 changes: 2 additions & 0 deletions lib/state/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export function createSessionState(): SessionState {
lastToolPrune: false,
lastCompaction: 0,
currentTurn: 0,
thresholdState: 0,
}
}

Expand All @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion lib/state/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}