Skip to content

Commit 1b6ef9b

Browse files
committed
Refactor: Inject prunable list into last assistant message instead of new user message
This change addresses models consistently responding to the injected prunable list even when instructed not to. By appending to the last assistant message instead of creating a new user message, the list appears as part of the model's own context state awareness rather than user input to respond to. - Rename injectUserMessage -> appendToLastAssistantMessage across all formats - Each format handler now finds and appends to the last assistant/model message - Update synthetic.txt prompt to reflect the new assistant message framing - Rename buildUserInjection -> buildAssistantInjection
1 parent c17fa92 commit 1b6ef9b

File tree

9 files changed

+94
-40
lines changed

9 files changed

+94
-40
lines changed

lib/fetch-wrapper/formats/anthropic.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,29 @@ 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
36+
appendToLastAssistantMessage(body: any, injection: string): boolean {
37+
if (!injection || !body.messages || body.messages.length === 0) return false
38+
39+
// Find the last assistant message
40+
for (let i = body.messages.length - 1; i >= 0; i--) {
41+
const msg = body.messages[i]
42+
if (msg.role === 'assistant') {
43+
// Append to existing content array
44+
if (Array.isArray(msg.content)) {
45+
msg.content.push({ type: 'text', text: injection })
46+
} else if (typeof msg.content === 'string') {
47+
// Convert string content to array format
48+
msg.content = [
49+
{ type: 'text', text: msg.content },
50+
{ type: 'text', text: injection }
51+
]
52+
} else {
53+
msg.content = [{ type: 'text', text: injection }]
54+
}
55+
return true
56+
}
57+
}
58+
return false
4359
},
4460

4561
extractToolOutputs(data: any[], state: PluginState): ToolOutput[] {

lib/fetch-wrapper/formats/bedrock.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,21 @@ 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
35+
appendToLastAssistantMessage(body: any, injection: string): boolean {
36+
if (!injection || !body.messages || body.messages.length === 0) return false
37+
38+
for (let i = body.messages.length - 1; i >= 0; i--) {
39+
const msg = body.messages[i]
40+
if (msg.role === 'assistant') {
41+
if (Array.isArray(msg.content)) {
42+
msg.content.push({ text: injection })
43+
} else {
44+
msg.content = [{ text: injection }]
45+
}
46+
return true
47+
}
48+
}
49+
return false
4250
},
4351

4452
extractToolOutputs(data: any[], state: PluginState): ToolOutput[] {

lib/fetch-wrapper/formats/gemini.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,21 @@ 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
34+
appendToLastAssistantMessage(body: any, injection: string): boolean {
35+
if (!injection || !body.contents || body.contents.length === 0) return false
36+
37+
for (let i = body.contents.length - 1; i >= 0; i--) {
38+
const content = body.contents[i]
39+
if (content.role === 'model') {
40+
if (Array.isArray(content.parts)) {
41+
content.parts.push({ text: injection })
42+
} else {
43+
content.parts = [{ text: injection }]
44+
}
45+
return true
46+
}
47+
}
48+
return false
4149
},
4250

4351
extractToolOutputs(data: any[], state: PluginState): ToolOutput[] {

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,23 @@ 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
30+
appendToLastAssistantMessage(body: any, injection: string): boolean {
31+
if (!injection || !body.messages || body.messages.length === 0) return false
32+
33+
for (let i = body.messages.length - 1; i >= 0; i--) {
34+
const msg = body.messages[i]
35+
if (msg.role === 'assistant') {
36+
if (typeof msg.content === 'string') {
37+
msg.content = msg.content + '\n\n' + injection
38+
} else if (Array.isArray(msg.content)) {
39+
msg.content.push({ type: 'text', text: injection })
40+
} else {
41+
msg.content = injection
42+
}
43+
return true
44+
}
45+
}
46+
return false
3447
},
3548

3649
extractToolOutputs(data: any[], state: PluginState): ToolOutput[] {

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

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,23 @@ 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
26+
appendToLastAssistantMessage(body: any, injection: string): boolean {
27+
if (!injection || !body.input || body.input.length === 0) return false
28+
29+
for (let i = body.input.length - 1; i >= 0; i--) {
30+
const item = body.input[i]
31+
if (item.type === 'message' && item.role === 'assistant') {
32+
if (typeof item.content === 'string') {
33+
item.content = item.content + '\n\n' + injection
34+
} else if (Array.isArray(item.content)) {
35+
item.content.push({ type: 'output_text', text: injection })
36+
} else {
37+
item.content = injection
38+
}
39+
return true
40+
}
41+
}
42+
return false
3443
},
3544

3645
extractToolOutputs(data: any[], state: PluginState): ToolOutput[] {

lib/fetch-wrapper/handler.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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, buildUserInjection } from "./prunable-list"
4+
import { buildPrunableToolsList, buildAssistantInjection } from "./prunable-list"
55
import { syncToolCache } from "../state/tool-cache"
66
import { loadPrompt } from "../core/prompt"
77

@@ -96,11 +96,11 @@ export async function handleFormat(
9696
modified = true
9797
}
9898

99-
const userInjection = buildUserInjection(prunableList, includeNudge)
99+
const assistantInjection = buildAssistantInjection(prunableList, includeNudge)
100100

101-
if (format.injectUserMessage && format.injectUserMessage(body, userInjection)) {
101+
if (format.appendToLastAssistantMessage && format.appendToLastAssistantMessage(body, assistantInjection)) {
102102
const nudgeMsg = includeNudge ? " with nudge" : ""
103-
ctx.logger.debug("fetch", `Injected prunable tools list${nudgeMsg} into user message (${format.name})`, {
103+
ctx.logger.debug("fetch", `Appended prunable tools list${nudgeMsg} to last assistant message (${format.name})`, {
104104
ids: numericIds,
105105
nudge: includeNudge,
106106
toolsSincePrune: ctx.toolTracker.toolResultCount

lib/fetch-wrapper/prunable-list.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export function buildPrunableToolsList(
4242
}
4343
}
4444

45-
export function buildUserInjection(
45+
export function buildAssistantInjection(
4646
prunableList: string,
4747
includeNudge: boolean
4848
): string {

lib/fetch-wrapper/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +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
22+
appendToLastAssistantMessage?(body: any, injection: string): boolean
2323
extractToolOutputs(data: any[], state: PluginState): ToolOutput[]
2424
replaceToolOutput(data: any[], toolId: string, prunedMessage: string, state: PluginState): boolean
2525
hasToolOutputs(data: any[]): boolean

lib/prompts/synthetic.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<system-reminder>
2-
The <prunable-tools> list and any pruning nudges are injected by a plugin and are invisible to the user.
2+
The <prunable-tools> list and any pruning nudges are injected by a plugin as assistant messages and are invisible to the user. Do NOT repeat, acknowledge, or respond to these in your output - simply use the information when deciding what to prune.
33

44
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.
55
</system-reminder>

0 commit comments

Comments
 (0)