Skip to content

Commit bf3e4d8

Browse files
fix: preserve tool_use blocks in summary for parallel tool calls (#9714)
Co-authored-by: huajiwuyan <[email protected]>
1 parent af74709 commit bf3e4d8

File tree

2 files changed

+148
-1
lines changed

2 files changed

+148
-1
lines changed

src/core/condense/__tests__/index.spec.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -828,6 +828,148 @@ describe("summarizeConversation", () => {
828828
expect(result.messages).toHaveLength(1 + 1 + N_MESSAGES_TO_KEEP) // first + summary + last 3
829829
expect(result.error).toBeUndefined()
830830
})
831+
832+
it("should include user tool_result message in summarize request when preserving tool_use blocks", async () => {
833+
const toolUseBlock = {
834+
type: "tool_use" as const,
835+
id: "toolu_history_fix",
836+
name: "read_file",
837+
input: { path: "sample.txt" },
838+
}
839+
const toolResultBlock = {
840+
type: "tool_result" as const,
841+
tool_use_id: "toolu_history_fix",
842+
content: "file contents",
843+
}
844+
845+
const messages: ApiMessage[] = [
846+
{ role: "user", content: "Hello", ts: 1 },
847+
{ role: "assistant", content: "Let me help", ts: 2 },
848+
{
849+
role: "assistant",
850+
content: [{ type: "text" as const, text: "Running tool..." }, toolUseBlock],
851+
ts: 3,
852+
},
853+
{
854+
role: "user",
855+
content: [toolResultBlock, { type: "text" as const, text: "Thanks" }],
856+
ts: 4,
857+
},
858+
{ role: "assistant", content: "Anything else?", ts: 5 },
859+
{ role: "user", content: "Nope", ts: 6 },
860+
]
861+
862+
let capturedRequestMessages: any[] | undefined
863+
const customStream = (async function* () {
864+
yield { type: "text" as const, text: "Summary of conversation" }
865+
yield { type: "usage" as const, totalCost: 0.05, outputTokens: 100 }
866+
})()
867+
868+
mockApiHandler.createMessage = vi.fn().mockImplementation((_prompt, requestMessagesParam) => {
869+
capturedRequestMessages = requestMessagesParam
870+
return customStream
871+
}) as any
872+
873+
const result = await summarizeConversation(
874+
messages,
875+
mockApiHandler,
876+
defaultSystemPrompt,
877+
taskId,
878+
DEFAULT_PREV_CONTEXT_TOKENS,
879+
false,
880+
undefined,
881+
undefined,
882+
true,
883+
)
884+
885+
expect(result.error).toBeUndefined()
886+
expect(capturedRequestMessages).toBeDefined()
887+
888+
const requestMessages = capturedRequestMessages!
889+
expect(requestMessages[requestMessages.length - 1]).toEqual({
890+
role: "user",
891+
content: "Summarize the conversation so far, as described in the prompt instructions.",
892+
})
893+
894+
const historyMessages = requestMessages.slice(0, -1)
895+
expect(historyMessages.length).toBeGreaterThanOrEqual(2)
896+
897+
const assistantMessage = historyMessages[historyMessages.length - 2]
898+
const userMessage = historyMessages[historyMessages.length - 1]
899+
900+
expect(assistantMessage.role).toBe("assistant")
901+
expect(Array.isArray(assistantMessage.content)).toBe(true)
902+
expect(
903+
(assistantMessage.content as any[]).some(
904+
(block) => block.type === "tool_use" && block.id === toolUseBlock.id,
905+
),
906+
).toBe(true)
907+
908+
expect(userMessage.role).toBe("user")
909+
expect(Array.isArray(userMessage.content)).toBe(true)
910+
expect(
911+
(userMessage.content as any[]).some(
912+
(block) => block.type === "tool_result" && block.tool_use_id === toolUseBlock.id,
913+
),
914+
).toBe(true)
915+
})
916+
917+
it("should append multiple tool_use blocks for parallel tool calls", async () => {
918+
const toolUseBlockA = {
919+
type: "tool_use" as const,
920+
id: "toolu_parallel_1",
921+
name: "search",
922+
input: { query: "foo" },
923+
}
924+
const toolUseBlockB = {
925+
type: "tool_use" as const,
926+
id: "toolu_parallel_2",
927+
name: "search",
928+
input: { query: "bar" },
929+
}
930+
931+
const messages: ApiMessage[] = [
932+
{ role: "user", content: "Start", ts: 1 },
933+
{ role: "assistant", content: "Working...", ts: 2 },
934+
{
935+
role: "assistant",
936+
content: [{ type: "text" as const, text: "Launching parallel tools" }, toolUseBlockA, toolUseBlockB],
937+
ts: 3,
938+
},
939+
{
940+
role: "user",
941+
content: [
942+
{ type: "tool_result" as const, tool_use_id: "toolu_parallel_1", content: "result A" },
943+
{ type: "tool_result" as const, tool_use_id: "toolu_parallel_2", content: "result B" },
944+
{ type: "text" as const, text: "Continue" },
945+
],
946+
ts: 4,
947+
},
948+
{ role: "assistant", content: "Processing results", ts: 5 },
949+
{ role: "user", content: "Thanks", ts: 6 },
950+
]
951+
952+
const result = await summarizeConversation(
953+
messages,
954+
mockApiHandler,
955+
defaultSystemPrompt,
956+
taskId,
957+
DEFAULT_PREV_CONTEXT_TOKENS,
958+
false,
959+
undefined,
960+
undefined,
961+
true,
962+
)
963+
964+
const summaryMessage = result.messages[1]
965+
expect(Array.isArray(summaryMessage.content)).toBe(true)
966+
const summaryContent = summaryMessage.content as any[]
967+
expect(summaryContent[0]).toEqual({ type: "text", text: "This is a summary" })
968+
969+
const preservedToolUses = summaryContent.filter((block) => block.type === "tool_use")
970+
expect(preservedToolUses).toHaveLength(2)
971+
expect(preservedToolUses.map((block) => block.id)).toEqual(["toolu_parallel_1", "toolu_parallel_2"])
972+
})
831973
})
832974

833975
describe("summarizeConversation with custom settings", () => {

src/core/condense/index.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,8 +172,13 @@ export async function summarizeConversation(
172172
? getKeepMessagesWithToolBlocks(messages, N_MESSAGES_TO_KEEP)
173173
: { keepMessages: messages.slice(-N_MESSAGES_TO_KEEP), toolUseBlocksToPreserve: [] }
174174

175+
const keepStartIndex = Math.max(messages.length - N_MESSAGES_TO_KEEP, 0)
176+
const includeFirstKeptMessageInSummary = toolUseBlocksToPreserve.length > 0
177+
const summarySliceEnd = includeFirstKeptMessageInSummary ? keepStartIndex + 1 : keepStartIndex
178+
const messagesBeforeKeep = summarySliceEnd > 0 ? messages.slice(0, summarySliceEnd) : []
179+
175180
// Get messages to summarize, including the first message and excluding the last N messages
176-
const messagesToSummarize = getMessagesSinceLastSummary(messages.slice(0, -N_MESSAGES_TO_KEEP))
181+
const messagesToSummarize = getMessagesSinceLastSummary(messagesBeforeKeep)
177182

178183
if (messagesToSummarize.length <= 1) {
179184
const error =

0 commit comments

Comments
 (0)