Skip to content
Merged
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
3 changes: 2 additions & 1 deletion lib/messages/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { prune, insertPruneToolContext } from "./prune"
export { prune } from "./prune"
export { insertPruneToolContext } from "./inject"
129 changes: 129 additions & 0 deletions lib/messages/inject.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type { SessionState, WithParts } from "../state"
import type { Logger } from "../logger"
import type { PluginConfig } from "../config"
import { loadPrompt } from "../prompt"
import { extractParameterKey, buildToolIdList, createSyntheticUserMessage } from "./utils"
import { getLastUserMessage } from "../shared-utils"

const getNudgeString = (config: PluginConfig): string => {
const discardEnabled = config.tools.discard.enabled
const extractEnabled = config.tools.extract.enabled

if (discardEnabled && extractEnabled) {
return loadPrompt(`user/nudge/nudge-both`)
} else if (discardEnabled) {
return loadPrompt(`user/nudge/nudge-discard`)
} else if (extractEnabled) {
return loadPrompt(`user/nudge/nudge-extract`)
}
return ""
}

const wrapPrunableTools = (content: string): string => `<prunable-tools>
The following tools have been invoked and are available for pruning. This list does not mandate immediate action. Consider your current goals and the resources you need before discarding valuable tool inputs or outputs. Consolidate your prunes for efficiency; it is rarely worth pruning a single tiny tool output. Keep the context free of noise.
${content}
</prunable-tools>`

const getCooldownMessage = (config: PluginConfig): string => {
const discardEnabled = config.tools.discard.enabled
const extractEnabled = config.tools.extract.enabled

let toolName: string
if (discardEnabled && extractEnabled) {
toolName = "discard or extract tools"
} else if (discardEnabled) {
toolName = "discard tool"
} else {
toolName = "extract tool"
}

return `<prunable-tools>
Context management was just performed. Do not use the ${toolName} again. A fresh list will be available after your next tool use.
</prunable-tools>`
}

const buildPrunableToolsList = (
state: SessionState,
config: PluginConfig,
logger: Logger,
messages: WithParts[],
): string => {
const lines: string[] = []
const toolIdList: string[] = buildToolIdList(state, messages, logger)

state.toolParameters.forEach((toolParameterEntry, toolCallId) => {
if (state.prune.toolIds.includes(toolCallId)) {
return
}

const allProtectedTools = config.tools.settings.protectedTools
if (allProtectedTools.includes(toolParameterEntry.tool)) {
return
}

const numericId = toolIdList.indexOf(toolCallId)
if (numericId === -1) {
logger.warn(`Tool in cache but not in toolIdList - possible stale entry`, {
toolCallId,
tool: toolParameterEntry.tool,
})
return
}
const paramKey = extractParameterKey(toolParameterEntry.tool, toolParameterEntry.parameters)
const description = paramKey
? `${toolParameterEntry.tool}, ${paramKey}`
: toolParameterEntry.tool
lines.push(`${numericId}: ${description}`)
logger.debug(
`Prunable tool found - ID: ${numericId}, Tool: ${toolParameterEntry.tool}, Call ID: ${toolCallId}`,
)
})

if (lines.length === 0) {
return ""
}

return wrapPrunableTools(lines.join("\n"))
}

export const insertPruneToolContext = (
state: SessionState,
config: PluginConfig,
logger: Logger,
messages: WithParts[],
): void => {
if (!config.tools.discard.enabled && !config.tools.extract.enabled) {
return
}

let prunableToolsContent: string

if (state.lastToolPrune) {
logger.debug("Last tool was prune - injecting cooldown message")
prunableToolsContent = getCooldownMessage(config)
} else {
const prunableToolsList = buildPrunableToolsList(state, config, logger, messages)
if (!prunableToolsList) {
return
}

logger.debug("prunable-tools: \n" + prunableToolsList)

let nudgeString = ""
if (
config.tools.settings.nudgeEnabled &&
state.nudgeCounter >= config.tools.settings.nudgeFrequency
) {
logger.info("Inserting prune nudge message")
nudgeString = "\n" + getNudgeString(config)
}

prunableToolsContent = prunableToolsList + nudgeString
}

const lastUserMessage = getLastUserMessage(messages)
if (!lastUserMessage) {
return
}
messages.push(createSyntheticUserMessage(lastUserMessage, prunableToolsContent))
}
127 changes: 1 addition & 126 deletions lib/messages/prune.ts
Original file line number Diff line number Diff line change
@@ -1,139 +1,14 @@
import type { SessionState, WithParts } from "../state"
import type { Logger } from "../logger"
import type { PluginConfig } from "../config"
import { loadPrompt } from "../prompt"
import { extractParameterKey, buildToolIdList, createSyntheticUserMessage } from "./utils"
import { getLastUserMessage, isMessageCompacted } from "../shared-utils"
import { isMessageCompacted } from "../shared-utils"

const PRUNED_TOOL_INPUT_REPLACEMENT =
"[content removed to save context, this is not what was written to the file, but a placeholder]"
const PRUNED_TOOL_OUTPUT_REPLACEMENT =
"[Output removed to save context - information superseded or no longer needed]"
const PRUNED_TOOL_ERROR_INPUT_REPLACEMENT = "[input removed due to failed tool call]"

const getNudgeString = (config: PluginConfig): string => {
const discardEnabled = config.tools.discard.enabled
const extractEnabled = config.tools.extract.enabled

if (discardEnabled && extractEnabled) {
return loadPrompt(`user/nudge/nudge-both`)
} else if (discardEnabled) {
return loadPrompt(`user/nudge/nudge-discard`)
} else if (extractEnabled) {
return loadPrompt(`user/nudge/nudge-extract`)
}
return ""
}

const wrapPrunableTools = (content: string): string => `<prunable-tools>
The following tools have been invoked and are available for pruning. This list does not mandate immediate action. Consider your current goals and the resources you need before discarding valuable tool inputs or outputs. Consolidate your prunes for efficiency; it is rarely worth pruning a single tiny tool output. Keep the context free of noise.
${content}
</prunable-tools>`

const getCooldownMessage = (config: PluginConfig): string => {
const discardEnabled = config.tools.discard.enabled
const extractEnabled = config.tools.extract.enabled

let toolName: string
if (discardEnabled && extractEnabled) {
toolName = "discard or extract tools"
} else if (discardEnabled) {
toolName = "discard tool"
} else {
toolName = "extract tool"
}

return `<prunable-tools>
Context management was just performed. Do not use the ${toolName} again. A fresh list will be available after your next tool use.
</prunable-tools>`
}

const buildPrunableToolsList = (
state: SessionState,
config: PluginConfig,
logger: Logger,
messages: WithParts[],
): string => {
const lines: string[] = []
const toolIdList: string[] = buildToolIdList(state, messages, logger)

state.toolParameters.forEach((toolParameterEntry, toolCallId) => {
if (state.prune.toolIds.includes(toolCallId)) {
return
}

const allProtectedTools = config.tools.settings.protectedTools
if (allProtectedTools.includes(toolParameterEntry.tool)) {
return
}

const numericId = toolIdList.indexOf(toolCallId)
if (numericId === -1) {
logger.warn(`Tool in cache but not in toolIdList - possible stale entry`, {
toolCallId,
tool: toolParameterEntry.tool,
})
return
}
const paramKey = extractParameterKey(toolParameterEntry.tool, toolParameterEntry.parameters)
const description = paramKey
? `${toolParameterEntry.tool}, ${paramKey}`
: toolParameterEntry.tool
lines.push(`${numericId}: ${description}`)
logger.debug(
`Prunable tool found - ID: ${numericId}, Tool: ${toolParameterEntry.tool}, Call ID: ${toolCallId}`,
)
})

if (lines.length === 0) {
return ""
}

return wrapPrunableTools(lines.join("\n"))
}

export const insertPruneToolContext = (
state: SessionState,
config: PluginConfig,
logger: Logger,
messages: WithParts[],
): void => {
if (!config.tools.discard.enabled && !config.tools.extract.enabled) {
return
}

let prunableToolsContent: string

if (state.lastToolPrune) {
logger.debug("Last tool was prune - injecting cooldown message")
prunableToolsContent = getCooldownMessage(config)
} else {
const prunableToolsList = buildPrunableToolsList(state, config, logger, messages)
if (!prunableToolsList) {
return
}

logger.debug("prunable-tools: \n" + prunableToolsList)

let nudgeString = ""
if (
config.tools.settings.nudgeEnabled &&
state.nudgeCounter >= config.tools.settings.nudgeFrequency
) {
logger.info("Inserting prune nudge message")
nudgeString = "\n" + getNudgeString(config)
}

prunableToolsContent = prunableToolsList + nudgeString
}

const lastUserMessage = getLastUserMessage(messages)
if (!lastUserMessage) {
return
}
messages.push(createSyntheticUserMessage(lastUserMessage, prunableToolsContent))
}

export const prune = (
state: SessionState,
logger: Logger,
Expand Down