Skip to content

Commit 4a749e3

Browse files
committed
Refactor: Move prunable tool list injection to user messages
- Split context management injection: core protocol remains in system message, but dynamic tool list moves to user message for better adherence - Add injectUserMessage support to all provider formats - Update synthetic prompt with system-reminder to prevent model from referencing invisible injected content
1 parent ca2835b commit 4a749e3

File tree

9 files changed

+65
-10
lines changed

9 files changed

+65
-10
lines changed

lib/fetch-wrapper/formats/anthropic.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,15 @@ export const anthropicFormat: FormatDescriptor = {
3333
return true
3434
},
3535

36+
injectUserMessage(body: any, injection: string): boolean {
37+
if (!injection || !body.messages) return false
38+
body.messages.push({
39+
role: 'user',
40+
content: [{ type: 'text', text: injection }]
41+
})
42+
return true
43+
},
44+
3645
extractToolOutputs(data: any[], state: PluginState): ToolOutput[] {
3746
const outputs: ToolOutput[] = []
3847

lib/fetch-wrapper/formats/bedrock.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ export const bedrockFormat: FormatDescriptor = {
3232
return true
3333
},
3434

35+
injectUserMessage(body: any, injection: string): boolean {
36+
if (!injection || !body.messages) return false
37+
body.messages.push({
38+
role: 'user',
39+
content: [{ text: injection }]
40+
})
41+
return true
42+
},
43+
3544
extractToolOutputs(data: any[], state: PluginState): ToolOutput[] {
3645
const outputs: ToolOutput[] = []
3746

lib/fetch-wrapper/formats/gemini.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ export const geminiFormat: FormatDescriptor = {
3131
return true
3232
},
3333

34+
injectUserMessage(body: any, injection: string): boolean {
35+
if (!injection || !body.contents) return false
36+
body.contents.push({
37+
role: 'user',
38+
parts: [{ text: injection }]
39+
})
40+
return true
41+
},
42+
3443
extractToolOutputs(data: any[], state: PluginState): ToolOutput[] {
3544
const outputs: ToolOutput[] = []
3645

lib/fetch-wrapper/formats/openai-chat.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ export const openaiChatFormat: FormatDescriptor = {
2727
return true
2828
},
2929

30+
injectUserMessage(body: any, injection: string): boolean {
31+
if (!injection || !body.messages) return false
32+
body.messages.push({ role: 'user', content: injection })
33+
return true
34+
},
35+
3036
extractToolOutputs(data: any[], state: PluginState): ToolOutput[] {
3137
const outputs: ToolOutput[] = []
3238

lib/fetch-wrapper/formats/openai-responses.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,16 @@ export const openaiResponsesFormat: FormatDescriptor = {
2323
return true
2424
},
2525

26+
injectUserMessage(body: any, injection: string): boolean {
27+
if (!injection || !body.input) return false
28+
body.input.push({
29+
type: 'message',
30+
role: 'user',
31+
content: injection
32+
})
33+
return true
34+
},
35+
2636
extractToolOutputs(data: any[], state: PluginState): ToolOutput[] {
2737
const outputs: ToolOutput[] = []
2838

lib/fetch-wrapper/handler.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { FetchHandlerContext, FetchHandlerResult, FormatDescriptor, PrunedIdData } from "./types"
22
import { type PluginState, ensureSessionRestored } from "../state"
33
import type { Logger } from "../logger"
4-
import { buildPrunableToolsList, buildSystemInjection } from "./prunable-list"
4+
import { buildPrunableToolsList, buildUserInjection } from "./prunable-list"
55
import { syncToolCache } from "../state/tool-cache"
6+
import { loadPrompt } from "../core/prompt"
67

8+
const SYNTHETIC_INSTRUCTION = loadPrompt("synthetic")
79
const PRUNED_CONTENT_MESSAGE = '[Output removed to save context - information superseded or no longer needed]'
810

911
function getMostRecentActiveSession(allSessions: any): any | undefined {
@@ -90,10 +92,15 @@ export async function handleFormat(
9092

9193
if (prunableList) {
9294
const includeNudge = ctx.config.nudge_freq > 0 && ctx.toolTracker.toolResultCount > ctx.config.nudge_freq
93-
const systemInjection = buildSystemInjection(prunableList, includeNudge)
94-
95-
if (format.injectSystemMessage(body, systemInjection)) {
96-
ctx.logger.debug("fetch", `Injected prunable tools list into system message (${format.name})`, {
95+
if (format.injectSystemMessage(body, SYNTHETIC_INSTRUCTION)) {
96+
modified = true
97+
}
98+
99+
const userInjection = buildUserInjection(prunableList, includeNudge)
100+
101+
if (format.injectUserMessage && format.injectUserMessage(body, userInjection)) {
102+
const nudgeMsg = includeNudge ? " with nudge" : ""
103+
ctx.logger.debug("fetch", `Injected prunable tools list${nudgeMsg} into user message (${format.name})`, {
97104
ids: numericIds,
98105
nudge: includeNudge,
99106
toolsSincePrune: ctx.toolTracker.toolResultCount

lib/fetch-wrapper/prunable-list.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { loadPrompt } from '../core/prompt'
44
import type { ToolMetadata } from './types'
55

66
const NUDGE_INSTRUCTION = loadPrompt("nudge")
7-
const SYNTHETIC_INSTRUCTION = loadPrompt("synthetic")
87

98
export interface PrunableListResult {
109
list: string
@@ -43,17 +42,16 @@ export function buildPrunableToolsList(
4342
}
4443
}
4544

46-
export function buildSystemInjection(
45+
export function buildUserInjection(
4746
prunableList: string,
4847
includeNudge: boolean
4948
): string {
5049
if (!prunableList) {
5150
return ''
5251
}
5352

54-
// Always include synthetic instruction, optionally add nudge
55-
const parts = [SYNTHETIC_INSTRUCTION, prunableList]
56-
53+
const parts = [prunableList]
54+
5755
if (includeNudge) {
5856
parts.push(NUDGE_INSTRUCTION)
5957
}

lib/fetch-wrapper/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface FormatDescriptor {
1919
detect(body: any): boolean
2020
getDataArray(body: any): any[] | undefined
2121
injectSystemMessage(body: any, injection: string): boolean
22+
injectUserMessage?(body: any, injection: string): boolean
2223
extractToolOutputs(data: any[], state: PluginState): ToolOutput[]
2324
replaceToolOutput(data: any[], toolId: string, prunedMessage: string, state: PluginState): boolean
2425
hasToolOutputs(data: any[]): boolean

lib/prompts/synthetic.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
<system-reminder>
2+
The <prunable-tools> list and any pruning nudges are injected by a plugin and are invisible to the user.
3+
4+
IMPORTANT: Your thinking/reasoning blocks must NOT reference, discuss, or address the <prunable-tools> list or any nudges about pruning. The user can see your thinking blocks, and referencing invisible plugin content will confuse them. In your thinking, focus only on the user's task and your problem-solving approach.
5+
</system-reminder>
6+
17
<instruction name=context_management_protocol>
28
You are operating in a context-constrained environment. You must actively manage your context window using the `prune` tool.
39

0 commit comments

Comments
 (0)