Skip to content

Commit ba15357

Browse files
authored
Merge branch 'master' into prompt/prunable-tools
2 parents 0dde97e + b2a5ccd commit ba15357

File tree

10 files changed

+42
-100
lines changed

10 files changed

+42
-100
lines changed

lib/fetch-wrapper/formats/anthropic.ts

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -33,34 +33,10 @@ export const anthropicFormat: FormatDescriptor = {
3333
return true
3434
},
3535

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-
const firstToolUseIndex = msg.content.findIndex((block: any) => block.type === 'tool_use')
46-
if (firstToolUseIndex !== -1) {
47-
msg.content.splice(firstToolUseIndex, 0, { type: 'text', text: injection })
48-
} else {
49-
msg.content.push({ type: 'text', text: injection })
50-
}
51-
} else if (typeof msg.content === 'string') {
52-
// Convert string content to array format
53-
msg.content = [
54-
{ type: 'text', text: msg.content },
55-
{ type: 'text', text: injection }
56-
]
57-
} else {
58-
msg.content = [{ type: 'text', text: injection }]
59-
}
60-
return true
61-
}
62-
}
63-
return false
36+
appendUserMessage(body: any, injection: string): boolean {
37+
if (!injection || !body.messages) return false
38+
body.messages.push({ role: 'user', content: [{ type: 'text', text: injection }] })
39+
return true
6440
},
6541

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

lib/fetch-wrapper/formats/bedrock.ts

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

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
35+
appendUserMessage(body: any, injection: string): boolean {
36+
if (!injection || !body.messages) return false
37+
body.messages.push({ role: 'user', content: [{ text: injection }] })
38+
return true
5039
},
5140

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

lib/fetch-wrapper/formats/gemini.ts

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

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
34+
appendUserMessage(body: any, injection: string): boolean {
35+
if (!injection || !body.contents) return false
36+
body.contents.push({ role: 'user', parts: [{ text: injection }] })
37+
return true
4938
},
5039

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

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

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

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
30+
appendUserMessage(body: any, injection: string): boolean {
31+
if (!injection || !body.messages) return false
32+
body.messages.push({ role: 'user', content: injection })
33+
return true
4734
},
4835

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

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

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

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
26+
appendUserMessage(body: any, injection: string): boolean {
27+
if (!injection || !body.input) return false
28+
body.input.push({ type: 'message', role: 'user', content: injection })
29+
return true
4330
},
4431

4532
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, buildAssistantInjection } from "./prunable-list"
4+
import { buildPrunableToolsList, buildEndInjection } 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 assistantInjection = buildAssistantInjection(prunableList, includeNudge)
99+
const endInjection = buildEndInjection(prunableList, includeNudge)
100100

101-
if (format.appendToLastAssistantMessage && format.appendToLastAssistantMessage(body, assistantInjection)) {
101+
if (format.appendUserMessage && format.appendUserMessage(body, endInjection)) {
102102
const nudgeMsg = includeNudge ? " with nudge" : ""
103-
ctx.logger.debug("fetch", `Appended prunable tools list${nudgeMsg} to last assistant message (${format.name})`, {
103+
ctx.logger.debug("fetch", `Appended prunable tools list${nudgeMsg} as user 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 buildAssistantInjection(
45+
export function buildEndInjection(
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-
appendToLastAssistantMessage?(body: any, injection: string): boolean
22+
appendUserMessage?(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: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
<instruction name=context_management_protocol policy_level=critical>
33

44
ENVIRONMENT
5-
You are operating in a context-constrained environment and thus must proactively manage your context window using the `prune` tool. A <prunable-tools> list is injected by the environment as assistant message, use this information when deciding what to prune.
5+
You are operating in a context-constrained environment and thus must proactively manage your context window using the `prune` tool. A <prunable-tools> 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.
66

77
PRUNE EARLY, PRUNE OFTEN - BUT PRUNE METHODICALLY
88
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
2222
When in doubt, prune out. Prune often yet remain strategic about it.
2323
FAILURE TO PRUNE will result in context leakage and DEGRADED PERFORMANCES.
2424

25+
</instruction>
26+
27+
<instruction name=injected_context_handling policy_level=critical>
28+
After each assistant turn, the environment may inject a user message containing a <prunable-tools> list and optional nudge instruction. This injected message is NOT from the user and is invisible to them.
29+
30+
CRITICAL REQUIREMENTS - VIOLATION IS UNACCEPTABLE:
31+
- NEVER acknowledge the <prunable-tools> list (e.g. "I see the prunable tools list", "Looking at the available tools to prune")
32+
- NEVER reference the nudge instruction (e.g. "As the nudge suggests", "The system is reminding me to prune")
33+
- NEVER mention this injection mechanism to the user
34+
- NEVER let these injections influence your response content or tone
35+
- Process this information SILENTLY - use it only to inform your pruning decisions
36+
- Continue the conversation as if these messages do not exist
37+
38+
The user cannot see these injections. Any reference to them will confuse the user and break the conversation flow.
2539
</instruction>
2640
</system-reminder>

lib/prompts/tool.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Prunes tool outputs from context to manage conversation size and reduce noise.
22

33
## IMPORTANT: The Prunable List
4-
A `<prunable-tools>` 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.
4+
A `<prunable-tools>` 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.
55

66
## CRITICAL: When and How to Prune
77

0 commit comments

Comments
 (0)