Skip to content

Commit 0042ef7

Browse files
committed
Fix context pruner to not remove user instructions prompt! And not leave extra messages
1 parent ba071c9 commit 0042ef7

File tree

4 files changed

+80
-33
lines changed

4 files changed

+80
-33
lines changed

.agents/__tests__/context-pruner.test.ts

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,12 @@ describe('context-pruner handleSteps', () => {
4747
})
4848
})
4949

50-
test('removes spawn_agent_inline call for context-pruner from last assistant message', () => {
50+
test('removes spawn_agent_inline call for context-pruner and following messages', () => {
5151
const messages = [
5252
createMessage('user', 'Hello'),
5353
createMessage('assistant', 'I will spawn the context-pruner agent.\n\n<codebuff_tool_call>\n{\n "cb_tool_name": "spawn_agent_inline",\n "agent_type": "context-pruner"\n}\n</codebuff_tool_call>'),
54+
createMessage('user', '{"params": {"maxContextLength": 100000}}'),
55+
createMessage('user', 'Tools and instructions'),
5456
]
5557

5658
const results = runHandleSteps(messages)
@@ -60,16 +62,31 @@ describe('context-pruner handleSteps', () => {
6062
expect(results[0].input.messages[0]).toEqual(createMessage('user', 'Hello'))
6163
})
6264

63-
test('does not remove last message if it does not contain context-pruner spawn call', () => {
65+
test('does not remove messages if assistant message does not contain context-pruner spawn call', () => {
6466
const messages = [
6567
createMessage('user', 'Hello'),
6668
createMessage('assistant', 'Regular response without spawn call'),
69+
createMessage('user', 'Follow up'),
70+
]
71+
72+
const results = runHandleSteps(messages)
73+
expect(results).toHaveLength(1)
74+
expect(results[0].input.messages).toHaveLength(3)
75+
})
76+
77+
test('handles context-pruner spawn call without enough following messages', () => {
78+
const messages = [
79+
createMessage('user', 'Hello'),
80+
createMessage('assistant', 'I will spawn the context-pruner agent.\n\n<codebuff_tool_call>\n{\n "cb_tool_name": "spawn_agent_inline",\n "agent_type": "context-pruner"\n}\n</codebuff_tool_call>'),
81+
createMessage('user', '{"params": {"maxContextLength": 100000}}'),
6782
]
6883

6984
const results = runHandleSteps(messages)
7085

7186
expect(results).toHaveLength(1)
72-
expect(results[0].input.messages).toHaveLength(2)
87+
// Should preserve all messages since there aren't 3 messages to remove
88+
expect(results[0].input.messages).toHaveLength(3)
89+
7390
})
7491

7592
test('removes old terminal command results while keeping recent 5', () => {
@@ -389,26 +406,41 @@ describe('context-pruner edge cases', () => {
389406

390407
test('handles spawn_agent_inline detection with variations', () => {
391408
const testCases = [
392-
'Regular message with spawn_agent_inline but not for context-pruner',
393-
'spawn_agent_inline call for "context-pruner" with quotes',
394-
'spawn_agent_inline\n "agent_type": "context-pruner"',
395-
'Multiple spawn_agent_inline calls, one for context-pruner',
409+
{
410+
content: 'Regular message with spawn_agent_inline but not for other-agent',
411+
shouldRemove: false,
412+
},
413+
{
414+
content: 'spawn_agent_inline call for "context-pruner" with quotes',
415+
shouldRemove: true, // Has context-pruner and 3 total messages
416+
},
417+
{
418+
content: 'spawn_agent_inline\n "agent_type": "context-pruner"',
419+
shouldRemove: true, // Has context-pruner and 3 total messages
420+
},
421+
{
422+
content: 'Multiple spawn_agent_inline calls, one for context-pruner',
423+
shouldRemove: true, // Has context-pruner and 3 total messages
424+
},
396425
]
397426

398-
testCases.forEach((content, index) => {
427+
testCases.forEach(({ content, shouldRemove }, index) => {
399428
const messages = [
400429
createMessage('user', 'Hello'),
401430
createMessage('assistant', content),
431+
createMessage('user', 'Follow up'),
432+
createMessage('user', 'Tools and instructions'),
402433
]
403434

404435
const results = runHandleSteps(messages)
405436

406-
if (content.includes('context-pruner')) {
407-
// Should remove the message containing context-pruner spawn
437+
if (shouldRemove) {
438+
// Should remove the assistant message and following 2 user messages
408439
expect(results[0].input.messages).toHaveLength(1)
440+
expect(results[0].input.messages[0]).toEqual(createMessage('user', 'Hello'))
409441
} else {
410-
// Should preserve the message
411-
expect(results[0].input.messages).toHaveLength(2)
442+
// Should preserve all messages
443+
expect(results[0].input.messages).toHaveLength(4)
412444
}
413445
})
414446
})

.agents/context-pruner.ts

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,23 +37,28 @@ const definition: AgentDefinition = {
3737
const maxMessageTokens: number = params?.maxContextLength ?? 200_000
3838
const numTerminalCommandsToKeep = 5
3939

40-
// Remove the last assistant message if it contains the spawn call that invoked this context-pruner
4140
let currentMessages = [...messages]
42-
if (currentMessages.length > 0) {
43-
const lastMessage = currentMessages[currentMessages.length - 1]
44-
if (
45-
lastMessage.role === 'assistant' &&
46-
typeof lastMessage.content === 'string'
47-
) {
48-
// Check if this message contains a spawn_agent_inline call for context-pruner
49-
if (
50-
lastMessage.content.includes('spawn_agent_inline') &&
51-
lastMessage.content.includes('context-pruner')
52-
) {
53-
// Remove the entire message
54-
currentMessages.pop()
55-
}
56-
}
41+
const lastAssistantMessageIndex = currentMessages.findLastIndex(
42+
(message) => message.role === 'assistant',
43+
)
44+
const lastAssistantMessage = currentMessages[lastAssistantMessageIndex]
45+
const lastAssistantMessageIsToolCall =
46+
typeof lastAssistantMessage?.content === 'string' &&
47+
lastAssistantMessage.content.includes('spawn_agent_inline') &&
48+
lastAssistantMessage.content.includes('context-pruner')
49+
const inputMessage = currentMessages[lastAssistantMessageIndex + 1]
50+
const userInstructionsToolsMessage =
51+
currentMessages[lastAssistantMessageIndex + 2]
52+
if (
53+
lastAssistantMessageIsToolCall &&
54+
inputMessage &&
55+
userInstructionsToolsMessage
56+
) {
57+
// Remove these messages:
58+
// - assistant message with spawn_agent_inline call
59+
// - input message with params object for this agent
60+
// - user instructions message with tools description
61+
currentMessages.splice(lastAssistantMessageIndex, 3)
5762
}
5863

5964
// Initial check - if already under limit, return (with inline agent tool call removed)

backend/src/__tests__/run-agent-step-tools.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -456,7 +456,7 @@ describe('runAgentStep - set_output tool', () => {
456456
// Delete the last two assistant messages by doing two iterations
457457
const messages = [...agentState.messageHistory]
458458

459-
// First iteration: find and remove the last assistant message
459+
// First iteration: find and remove the last assistant message, which is the tool call to this agent
460460
for (let i = messages.length - 1; i >= 0; i--) {
461461
if (messages[i].role === 'assistant') {
462462
messages.splice(i, 1)
@@ -472,6 +472,14 @@ describe('runAgentStep - set_output tool', () => {
472472
}
473473
}
474474

475+
// Third iteration: find and remove the third assistant message
476+
for (let i = messages.length - 1; i >= 0; i--) {
477+
if (messages[i].role === 'assistant') {
478+
messages.splice(i, 1)
479+
break
480+
}
481+
}
482+
475483
// Set the updated messages
476484
yield {
477485
toolName: 'set_messages',
@@ -551,7 +559,7 @@ describe('runAgentStep - set_output tool', () => {
551559
{ role: 'user', content: 'Hello' },
552560
{ role: 'assistant', content: 'Hi there!' },
553561
{ role: 'user', content: 'How are you?' },
554-
{ role: 'assistant', content: 'I am doing well, thank you!' },
562+
// { role: 'assistant', content: 'I am doing well, thank you!' },
555563
{ role: 'user', content: 'Can you help me?' },
556564
{
557565
role: 'user',
@@ -565,6 +573,11 @@ describe('runAgentStep - set_output tool', () => {
565573
'Delete the last two assistant messages',
566574
),
567575
},
576+
{
577+
role: 'user',
578+
content: expect.stringContaining('Delete messages instructions prompt'),
579+
timeToLive: 'userPrompt',
580+
},
568581
]
569582

570583
expectedMessages.forEach((expected, index) => {

backend/src/tools/handlers/tool/spawn-agent-inline.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,6 @@ export const handleSpawnAgentInline = ((params: {
112112
// Since we share the same message array reference, this should already be updated
113113
let finalMessages = result.agentState?.messageHistory || state.messages
114114

115-
// Expire messages with timeToLive: 'userPrompt' to clean up inline agent's temporary messages
116-
finalMessages = expireMessages(finalMessages, 'userPrompt')
117-
118115
state.messages = finalMessages
119116

120117
// Update parent agent state to reflect shared message history

0 commit comments

Comments
 (0)