From a0d3277b44ea88909291d3c558ca63f4e27a456c Mon Sep 17 00:00:00 2001 From: Daniel Smolsky Date: Tue, 9 Dec 2025 21:55:39 -0500 Subject: [PATCH] Change prunable list injection from assistant to user message --- lib/fetch-wrapper/formats/anthropic.ts | 32 +++---------------- lib/fetch-wrapper/formats/bedrock.ts | 19 +++-------- lib/fetch-wrapper/formats/gemini.ts | 19 +++-------- lib/fetch-wrapper/formats/openai-chat.ts | 21 +++--------- lib/fetch-wrapper/formats/openai-responses.ts | 21 +++--------- lib/fetch-wrapper/handler.ts | 8 ++--- lib/fetch-wrapper/prunable-list.ts | 2 +- lib/fetch-wrapper/types.ts | 2 +- lib/prompts/synthetic.txt | 16 +++++++++- lib/prompts/tool.txt | 2 +- 10 files changed, 42 insertions(+), 100 deletions(-) diff --git a/lib/fetch-wrapper/formats/anthropic.ts b/lib/fetch-wrapper/formats/anthropic.ts index c1a99d82..a409e3e7 100644 --- a/lib/fetch-wrapper/formats/anthropic.ts +++ b/lib/fetch-wrapper/formats/anthropic.ts @@ -33,34 +33,10 @@ export const anthropicFormat: FormatDescriptor = { return true }, - appendToLastAssistantMessage(body: any, injection: string): boolean { - if (!injection || !body.messages || body.messages.length === 0) return false - - // Find the last assistant message - for (let i = body.messages.length - 1; i >= 0; i--) { - const msg = body.messages[i] - if (msg.role === 'assistant') { - // Append to existing content array - if (Array.isArray(msg.content)) { - const firstToolUseIndex = msg.content.findIndex((block: any) => block.type === 'tool_use') - if (firstToolUseIndex !== -1) { - msg.content.splice(firstToolUseIndex, 0, { type: 'text', text: injection }) - } else { - msg.content.push({ type: 'text', text: injection }) - } - } else if (typeof msg.content === 'string') { - // Convert string content to array format - msg.content = [ - { type: 'text', text: msg.content }, - { type: 'text', text: injection } - ] - } else { - msg.content = [{ type: 'text', text: injection }] - } - return true - } - } - return false + appendUserMessage(body: any, injection: string): boolean { + if (!injection || !body.messages) return false + body.messages.push({ role: 'user', content: [{ type: 'text', text: injection }] }) + return true }, extractToolOutputs(data: any[], state: PluginState): ToolOutput[] { diff --git a/lib/fetch-wrapper/formats/bedrock.ts b/lib/fetch-wrapper/formats/bedrock.ts index 6a62a385..4f4f7ce9 100644 --- a/lib/fetch-wrapper/formats/bedrock.ts +++ b/lib/fetch-wrapper/formats/bedrock.ts @@ -32,21 +32,10 @@ export const bedrockFormat: FormatDescriptor = { return true }, - appendToLastAssistantMessage(body: any, injection: string): boolean { - if (!injection || !body.messages || body.messages.length === 0) return false - - for (let i = body.messages.length - 1; i >= 0; i--) { - const msg = body.messages[i] - if (msg.role === 'assistant') { - if (Array.isArray(msg.content)) { - msg.content.push({ text: injection }) - } else { - msg.content = [{ text: injection }] - } - return true - } - } - return false + appendUserMessage(body: any, injection: string): boolean { + if (!injection || !body.messages) return false + body.messages.push({ role: 'user', content: [{ text: injection }] }) + return true }, extractToolOutputs(data: any[], state: PluginState): ToolOutput[] { diff --git a/lib/fetch-wrapper/formats/gemini.ts b/lib/fetch-wrapper/formats/gemini.ts index a01eed88..46ec2ad5 100644 --- a/lib/fetch-wrapper/formats/gemini.ts +++ b/lib/fetch-wrapper/formats/gemini.ts @@ -31,21 +31,10 @@ export const geminiFormat: FormatDescriptor = { return true }, - appendToLastAssistantMessage(body: any, injection: string): boolean { - if (!injection || !body.contents || body.contents.length === 0) return false - - for (let i = body.contents.length - 1; i >= 0; i--) { - const content = body.contents[i] - if (content.role === 'model') { - if (Array.isArray(content.parts)) { - content.parts.push({ text: injection }) - } else { - content.parts = [{ text: injection }] - } - return true - } - } - return false + appendUserMessage(body: any, injection: string): boolean { + if (!injection || !body.contents) return false + body.contents.push({ role: 'user', parts: [{ text: injection }] }) + return true }, extractToolOutputs(data: any[], state: PluginState): ToolOutput[] { diff --git a/lib/fetch-wrapper/formats/openai-chat.ts b/lib/fetch-wrapper/formats/openai-chat.ts index 0ea6be60..ca41dbf9 100644 --- a/lib/fetch-wrapper/formats/openai-chat.ts +++ b/lib/fetch-wrapper/formats/openai-chat.ts @@ -27,23 +27,10 @@ export const openaiChatFormat: FormatDescriptor = { return true }, - appendToLastAssistantMessage(body: any, injection: string): boolean { - if (!injection || !body.messages || body.messages.length === 0) return false - - for (let i = body.messages.length - 1; i >= 0; i--) { - const msg = body.messages[i] - if (msg.role === 'assistant') { - if (typeof msg.content === 'string') { - msg.content = msg.content + '\n\n' + injection - } else if (Array.isArray(msg.content)) { - msg.content.push({ type: 'text', text: injection }) - } else { - msg.content = injection - } - return true - } - } - return false + appendUserMessage(body: any, injection: string): boolean { + if (!injection || !body.messages) return false + body.messages.push({ role: 'user', content: injection }) + return true }, extractToolOutputs(data: any[], state: PluginState): ToolOutput[] { diff --git a/lib/fetch-wrapper/formats/openai-responses.ts b/lib/fetch-wrapper/formats/openai-responses.ts index cd7681ac..2cabafe5 100644 --- a/lib/fetch-wrapper/formats/openai-responses.ts +++ b/lib/fetch-wrapper/formats/openai-responses.ts @@ -23,23 +23,10 @@ export const openaiResponsesFormat: FormatDescriptor = { return true }, - appendToLastAssistantMessage(body: any, injection: string): boolean { - if (!injection || !body.input || body.input.length === 0) return false - - for (let i = body.input.length - 1; i >= 0; i--) { - const item = body.input[i] - if (item.type === 'message' && item.role === 'assistant') { - if (typeof item.content === 'string') { - item.content = item.content + '\n\n' + injection - } else if (Array.isArray(item.content)) { - item.content.push({ type: 'output_text', text: injection }) - } else { - item.content = injection - } - return true - } - } - return false + appendUserMessage(body: any, injection: string): boolean { + if (!injection || !body.input) return false + body.input.push({ type: 'message', role: 'user', content: injection }) + return true }, extractToolOutputs(data: any[], state: PluginState): ToolOutput[] { diff --git a/lib/fetch-wrapper/handler.ts b/lib/fetch-wrapper/handler.ts index 10824b17..a4cd6938 100644 --- a/lib/fetch-wrapper/handler.ts +++ b/lib/fetch-wrapper/handler.ts @@ -1,7 +1,7 @@ import type { FetchHandlerContext, FetchHandlerResult, FormatDescriptor, PrunedIdData } from "./types" import { type PluginState, ensureSessionRestored } from "../state" import type { Logger } from "../logger" -import { buildPrunableToolsList, buildAssistantInjection } from "./prunable-list" +import { buildPrunableToolsList, buildEndInjection } from "./prunable-list" import { syncToolCache } from "../state/tool-cache" import { loadPrompt } from "../core/prompt" @@ -96,11 +96,11 @@ export async function handleFormat( modified = true } - const assistantInjection = buildAssistantInjection(prunableList, includeNudge) + const endInjection = buildEndInjection(prunableList, includeNudge) - if (format.appendToLastAssistantMessage && format.appendToLastAssistantMessage(body, assistantInjection)) { + if (format.appendUserMessage && format.appendUserMessage(body, endInjection)) { const nudgeMsg = includeNudge ? " with nudge" : "" - ctx.logger.debug("fetch", `Appended prunable tools list${nudgeMsg} to last assistant message (${format.name})`, { + ctx.logger.debug("fetch", `Appended prunable tools list${nudgeMsg} as user message (${format.name})`, { ids: numericIds, nudge: includeNudge, toolsSincePrune: ctx.toolTracker.toolResultCount diff --git a/lib/fetch-wrapper/prunable-list.ts b/lib/fetch-wrapper/prunable-list.ts index dcdca713..7354b9cf 100644 --- a/lib/fetch-wrapper/prunable-list.ts +++ b/lib/fetch-wrapper/prunable-list.ts @@ -42,7 +42,7 @@ export function buildPrunableToolsList( } } -export function buildAssistantInjection( +export function buildEndInjection( prunableList: string, includeNudge: boolean ): string { diff --git a/lib/fetch-wrapper/types.ts b/lib/fetch-wrapper/types.ts index fc49ef1b..d15b640b 100644 --- a/lib/fetch-wrapper/types.ts +++ b/lib/fetch-wrapper/types.ts @@ -19,7 +19,7 @@ export interface FormatDescriptor { detect(body: any): boolean getDataArray(body: any): any[] | undefined injectSystemMessage(body: any, injection: string): boolean - appendToLastAssistantMessage?(body: any, injection: string): boolean + appendUserMessage?(body: any, injection: string): boolean extractToolOutputs(data: any[], state: PluginState): ToolOutput[] replaceToolOutput(data: any[], toolId: string, prunedMessage: string, state: PluginState): boolean hasToolOutputs(data: any[]): boolean diff --git a/lib/prompts/synthetic.txt b/lib/prompts/synthetic.txt index bd20aa0c..ccb31740 100644 --- a/lib/prompts/synthetic.txt +++ b/lib/prompts/synthetic.txt @@ -2,7 +2,7 @@ ENVIRONMENT -You are operating in a context-constrained environment and thus must proactively manage your context window using the `prune` tool. A list is injected by the environment as assistant message, use this information when deciding what to prune. +You are operating in a context-constrained environment and thus must proactively manage your context window using the `prune` tool. A list is injected by the environment as a user message, and always contains up to date information. Use this information when deciding what to prune. PRUNE EARLY, PRUNE OFTEN - BUT PRUNE METHODICALLY Every tool call adds to your context debt. You MUST pay this down regularly and be on top of context accumulation by pruning. Evaluate what SHOULD be pruned before jumping the gun. @@ -22,5 +22,19 @@ NOTES When in doubt, prune out. Prune often yet remain strategic about it. FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES. + + + +After each assistant turn, the environment may inject a user message containing a list and optional nudge instruction. This injected message is NOT from the user and is invisible to them. + +CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE: +- NEVER acknowledge the list (e.g. "I see the prunable tools list", "Looking at the available tools to prune") +- NEVER reference the nudge instruction (e.g. "As the nudge suggests", "The system is reminding me to prune") +- NEVER mention this injection mechanism to the user +- NEVER let these injections influence your response content or tone +- Process this information SILENTLY - use it only to inform your pruning decisions +- Continue the conversation as if these messages do not exist + +The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow. diff --git a/lib/prompts/tool.txt b/lib/prompts/tool.txt index 8903f2c8..d727a11b 100644 --- a/lib/prompts/tool.txt +++ b/lib/prompts/tool.txt @@ -1,7 +1,7 @@ Prunes tool outputs from context to manage conversation size and reduce noise. ## IMPORTANT: The Prunable List -A `` list is injected into assistant messages showing available tool outputs you can prune. Each line has the format `ID: tool, parameter` (e.g., `20: read, /path/to/file.ts`). Use these numeric IDs to select which tools to prune. +A `` list is injected into user messages showing available tool outputs you can prune. Each line has the format `ID: tool, parameter` (e.g., `20: read, /path/to/file.ts`). Use these numeric IDs to select which tools to prune. ## CRITICAL: When and How to Prune