mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: preserve reasoning_content in condense summary for DeepSeek-reasoner (#10292)
This commit is contained in:
parent
a8ac2ced02
commit
89e9261367
4 changed files with 354 additions and 30 deletions
|
|
@ -86,7 +86,19 @@ describe("Condense", () => {
|
|||
// Verify we have a summary message
|
||||
const summaryMessage = result.messages.find((msg) => msg.isSummary)
|
||||
expect(summaryMessage).toBeTruthy()
|
||||
expect(summaryMessage?.content).toBe("Mock summary of the conversation")
|
||||
// Summary content is now always an array with a synthetic reasoning block + text block
|
||||
// for DeepSeek-reasoner compatibility
|
||||
expect(Array.isArray(summaryMessage?.content)).toBe(true)
|
||||
const contentArray = summaryMessage?.content as Anthropic.Messages.ContentBlockParam[]
|
||||
expect(contentArray).toHaveLength(2)
|
||||
expect(contentArray[0]).toEqual({
|
||||
type: "reasoning",
|
||||
text: "Condensing conversation context. The summary below captures the key information from the prior conversation.",
|
||||
})
|
||||
expect(contentArray[1]).toEqual({
|
||||
type: "text",
|
||||
text: "Mock summary of the conversation",
|
||||
})
|
||||
|
||||
// With non-destructive condensing, all messages are retained (tagged but not deleted)
|
||||
// Use getEffectiveApiHistory to verify the effective view matches the old behavior
|
||||
|
|
|
|||
|
|
@ -246,6 +246,94 @@ describe("getKeepMessagesWithToolBlocks", () => {
|
|||
expect(result.keepMessages).toEqual(messages)
|
||||
expect(result.toolUseBlocksToPreserve).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should preserve reasoning blocks alongside tool_use blocks for DeepSeek/Z.ai interleaved thinking", () => {
|
||||
const reasoningBlock = {
|
||||
type: "reasoning" as const,
|
||||
text: "Let me think about this step by step...",
|
||||
}
|
||||
const toolUseBlock = {
|
||||
type: "tool_use" as const,
|
||||
id: "toolu_deepseek_123",
|
||||
name: "read_file",
|
||||
input: { path: "test.txt" },
|
||||
}
|
||||
const toolResultBlock = {
|
||||
type: "tool_result" as const,
|
||||
tool_use_id: "toolu_deepseek_123",
|
||||
content: "file contents",
|
||||
}
|
||||
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Let me help", ts: 2 },
|
||||
{ role: "user", content: "Please read the file", ts: 3 },
|
||||
{
|
||||
role: "assistant",
|
||||
// DeepSeek stores reasoning as content blocks alongside tool_use
|
||||
content: [reasoningBlock as any, { type: "text" as const, text: "Reading file..." }, toolUseBlock],
|
||||
ts: 4,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [toolResultBlock, { type: "text" as const, text: "Continue" }],
|
||||
ts: 5,
|
||||
},
|
||||
{ role: "assistant", content: "Got it, the file says...", ts: 6 },
|
||||
{ role: "user", content: "Thanks", ts: 7 },
|
||||
]
|
||||
|
||||
const result = getKeepMessagesWithToolBlocks(messages, 3)
|
||||
|
||||
// keepMessages should be the last 3 messages
|
||||
expect(result.keepMessages).toHaveLength(3)
|
||||
expect(result.keepMessages[0].ts).toBe(5)
|
||||
|
||||
// Should preserve the tool_use block
|
||||
expect(result.toolUseBlocksToPreserve).toHaveLength(1)
|
||||
expect(result.toolUseBlocksToPreserve[0]).toEqual(toolUseBlock)
|
||||
|
||||
// Should preserve the reasoning block for DeepSeek/Z.ai interleaved thinking
|
||||
expect(result.reasoningBlocksToPreserve).toHaveLength(1)
|
||||
expect((result.reasoningBlocksToPreserve[0] as any).type).toBe("reasoning")
|
||||
expect((result.reasoningBlocksToPreserve[0] as any).text).toBe("Let me think about this step by step...")
|
||||
})
|
||||
|
||||
it("should return empty reasoningBlocksToPreserve when no reasoning blocks present", () => {
|
||||
const toolUseBlock = {
|
||||
type: "tool_use" as const,
|
||||
id: "toolu_123",
|
||||
name: "read_file",
|
||||
input: { path: "test.txt" },
|
||||
}
|
||||
const toolResultBlock = {
|
||||
type: "tool_result" as const,
|
||||
tool_use_id: "toolu_123",
|
||||
content: "file contents",
|
||||
}
|
||||
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{
|
||||
role: "assistant",
|
||||
// No reasoning block, just text and tool_use
|
||||
content: [{ type: "text" as const, text: "Reading file..." }, toolUseBlock],
|
||||
ts: 2,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [toolResultBlock],
|
||||
ts: 3,
|
||||
},
|
||||
{ role: "assistant", content: "Done", ts: 4 },
|
||||
{ role: "user", content: "Thanks", ts: 5 },
|
||||
]
|
||||
|
||||
const result = getKeepMessagesWithToolBlocks(messages, 3)
|
||||
|
||||
expect(result.toolUseBlocksToPreserve).toHaveLength(1)
|
||||
expect(result.reasoningBlocksToPreserve).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getMessagesSinceLastSummary", () => {
|
||||
|
|
@ -422,7 +510,14 @@ describe("summarizeConversation", () => {
|
|||
const summaryMessage = result.messages.find((m) => m.isSummary)
|
||||
expect(summaryMessage).toBeDefined()
|
||||
expect(summaryMessage!.role).toBe("assistant")
|
||||
expect(summaryMessage!.content).toBe("This is a summary")
|
||||
// Summary content is now always an array with [synthetic reasoning, text]
|
||||
// for DeepSeek-reasoner compatibility (requires reasoning_content on all assistant messages)
|
||||
expect(Array.isArray(summaryMessage!.content)).toBe(true)
|
||||
const content = summaryMessage!.content as any[]
|
||||
expect(content).toHaveLength(2)
|
||||
expect(content[0].type).toBe("reasoning")
|
||||
expect(content[1].type).toBe("text")
|
||||
expect(content[1].text).toBe("This is a summary")
|
||||
expect(summaryMessage!.isSummary).toBe(true)
|
||||
|
||||
// Verify that the effective API history matches expected: first + summary + last N messages
|
||||
|
|
@ -827,14 +922,16 @@ describe("summarizeConversation", () => {
|
|||
expect(summaryMessage!.isSummary).toBe(true)
|
||||
expect(Array.isArray(summaryMessage!.content)).toBe(true)
|
||||
|
||||
// Content should be [text block, tool_use block]
|
||||
// Content should be [synthetic reasoning, text block, tool_use block]
|
||||
// The synthetic reasoning is always added for DeepSeek-reasoner compatibility
|
||||
const content = summaryMessage!.content as Anthropic.Messages.ContentBlockParam[]
|
||||
expect(content).toHaveLength(2)
|
||||
expect(content[0].type).toBe("text")
|
||||
expect((content[0] as Anthropic.Messages.TextBlockParam).text).toBe("Summary of conversation")
|
||||
expect(content[1].type).toBe("tool_use")
|
||||
expect((content[1] as Anthropic.Messages.ToolUseBlockParam).id).toBe("toolu_123")
|
||||
expect((content[1] as Anthropic.Messages.ToolUseBlockParam).name).toBe("read_file")
|
||||
expect(content).toHaveLength(3)
|
||||
expect((content[0] as any).type).toBe("reasoning") // Synthetic reasoning for DeepSeek
|
||||
expect(content[1].type).toBe("text")
|
||||
expect((content[1] as Anthropic.Messages.TextBlockParam).text).toBe("Summary of conversation")
|
||||
expect(content[2].type).toBe("tool_use")
|
||||
expect((content[2] as Anthropic.Messages.ToolUseBlockParam).id).toBe("toolu_123")
|
||||
expect((content[2] as Anthropic.Messages.ToolUseBlockParam).name).toBe("read_file")
|
||||
|
||||
// With non-destructive condensing, all messages are retained plus the summary
|
||||
expect(result.messages.length).toBe(messages.length + 1) // all original + summary
|
||||
|
|
@ -981,7 +1078,10 @@ describe("summarizeConversation", () => {
|
|||
expect(summaryMessage).toBeDefined()
|
||||
expect(Array.isArray(summaryMessage!.content)).toBe(true)
|
||||
const summaryContent = summaryMessage!.content as Anthropic.Messages.ContentBlockParam[]
|
||||
expect(summaryContent[0]).toEqual({ type: "text", text: "This is a summary" })
|
||||
// First block is synthetic reasoning for DeepSeek-reasoner compatibility
|
||||
expect((summaryContent[0] as any).type).toBe("reasoning")
|
||||
// Second block is the text summary
|
||||
expect(summaryContent[1]).toEqual({ type: "text", text: "This is a summary" })
|
||||
|
||||
const preservedToolUses = summaryContent.filter(
|
||||
(block): block is Anthropic.Messages.ToolUseBlockParam => block.type === "tool_use",
|
||||
|
|
@ -989,6 +1089,153 @@ describe("summarizeConversation", () => {
|
|||
expect(preservedToolUses).toHaveLength(2)
|
||||
expect(preservedToolUses.map((block) => block.id)).toEqual(["toolu_parallel_1", "toolu_parallel_2"])
|
||||
})
|
||||
|
||||
it("should preserve reasoning blocks in summary message for DeepSeek/Z.ai interleaved thinking", async () => {
|
||||
const reasoningBlock = {
|
||||
type: "reasoning" as const,
|
||||
text: "Let me think about this step by step...",
|
||||
}
|
||||
const toolUseBlock = {
|
||||
type: "tool_use" as const,
|
||||
id: "toolu_deepseek_reason",
|
||||
name: "read_file",
|
||||
input: { path: "test.txt" },
|
||||
}
|
||||
const toolResultBlock = {
|
||||
type: "tool_result" as const,
|
||||
tool_use_id: "toolu_deepseek_reason",
|
||||
content: "file contents",
|
||||
}
|
||||
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "Hello", ts: 1 },
|
||||
{ role: "assistant", content: "Let me help", ts: 2 },
|
||||
{ role: "user", content: "Please read the file", ts: 3 },
|
||||
{
|
||||
role: "assistant",
|
||||
// DeepSeek stores reasoning as content blocks alongside tool_use
|
||||
content: [reasoningBlock as any, { type: "text" as const, text: "Reading file..." }, toolUseBlock],
|
||||
ts: 4,
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [toolResultBlock, { type: "text" as const, text: "Continue" }],
|
||||
ts: 5,
|
||||
},
|
||||
{ role: "assistant", content: "Got it, the file says...", ts: 6 },
|
||||
{ role: "user", content: "Thanks", ts: 7 },
|
||||
]
|
||||
|
||||
// Create a stream with usage information
|
||||
const streamWithUsage = (async function* () {
|
||||
yield { type: "text" as const, text: "Summary of conversation" }
|
||||
yield { type: "usage" as const, totalCost: 0.05, outputTokens: 100 }
|
||||
})()
|
||||
|
||||
mockApiHandler.createMessage = vi.fn().mockReturnValue(streamWithUsage) as any
|
||||
mockApiHandler.countTokens = vi.fn().mockImplementation(() => Promise.resolve(50)) as any
|
||||
|
||||
const result = await summarizeConversation(
|
||||
messages,
|
||||
mockApiHandler,
|
||||
defaultSystemPrompt,
|
||||
taskId,
|
||||
DEFAULT_PREV_CONTEXT_TOKENS,
|
||||
false, // isAutomaticTrigger
|
||||
undefined, // customCondensingPrompt
|
||||
undefined, // condensingApiHandler
|
||||
true, // useNativeTools - required for tool_use block preservation
|
||||
)
|
||||
|
||||
// Find the summary message
|
||||
const summaryMessage = result.messages.find((m) => m.isSummary)
|
||||
expect(summaryMessage).toBeDefined()
|
||||
expect(summaryMessage!.role).toBe("assistant")
|
||||
expect(summaryMessage!.isSummary).toBe(true)
|
||||
expect(Array.isArray(summaryMessage!.content)).toBe(true)
|
||||
|
||||
// Content should be [synthetic reasoning, preserved reasoning, text block, tool_use block]
|
||||
// - Synthetic reasoning is always added for DeepSeek-reasoner compatibility
|
||||
// - Preserved reasoning from the condensed assistant message
|
||||
// This order ensures reasoning_content is always present for DeepSeek/Z.ai
|
||||
const content = summaryMessage!.content as Anthropic.Messages.ContentBlockParam[]
|
||||
expect(content).toHaveLength(4)
|
||||
|
||||
// First block should be synthetic reasoning
|
||||
expect((content[0] as any).type).toBe("reasoning")
|
||||
expect((content[0] as any).text).toContain("Condensing conversation context")
|
||||
|
||||
// Second block should be preserved reasoning from the condensed message
|
||||
expect((content[1] as any).type).toBe("reasoning")
|
||||
expect((content[1] as any).text).toBe("Let me think about this step by step...")
|
||||
|
||||
// Third block should be text (the summary)
|
||||
expect(content[2].type).toBe("text")
|
||||
expect((content[2] as Anthropic.Messages.TextBlockParam).text).toBe("Summary of conversation")
|
||||
|
||||
// Fourth block should be tool_use
|
||||
expect(content[3].type).toBe("tool_use")
|
||||
expect((content[3] as Anthropic.Messages.ToolUseBlockParam).id).toBe("toolu_deepseek_reason")
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should include synthetic reasoning block in summary for DeepSeek-reasoner compatibility even without tool_use blocks", async () => {
|
||||
// This test verifies the fix for the DeepSeek-reasoner 400 error:
|
||||
// "Missing `reasoning_content` field in the assistant message at message index 1"
|
||||
// DeepSeek-reasoner requires reasoning_content on ALL assistant messages, not just those with tool_calls.
|
||||
// After condensation, the summary becomes an assistant message that needs reasoning_content.
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "Tell me a joke", ts: 1 },
|
||||
{ role: "assistant", content: "Why did the programmer quit?", ts: 2 },
|
||||
{ role: "user", content: "I don't know, why?", ts: 3 },
|
||||
{ role: "assistant", content: "He didn't get arrays!", ts: 4 },
|
||||
{ role: "user", content: "Another one please", ts: 5 },
|
||||
{ role: "assistant", content: "Why do programmers prefer dark mode?", ts: 6 },
|
||||
{ role: "user", content: "Why?", ts: 7 },
|
||||
]
|
||||
|
||||
// Create a stream with usage information (no tool calls in this conversation)
|
||||
const streamWithUsage = (async function* () {
|
||||
yield { type: "text" as const, text: "Summary: User requested jokes." }
|
||||
yield { type: "usage" as const, totalCost: 0.05, outputTokens: 100 }
|
||||
})()
|
||||
|
||||
mockApiHandler.createMessage = vi.fn().mockReturnValue(streamWithUsage) as any
|
||||
mockApiHandler.countTokens = vi.fn().mockImplementation(() => Promise.resolve(50)) as any
|
||||
|
||||
const result = await summarizeConversation(
|
||||
messages,
|
||||
mockApiHandler,
|
||||
defaultSystemPrompt,
|
||||
taskId,
|
||||
DEFAULT_PREV_CONTEXT_TOKENS,
|
||||
false, // isAutomaticTrigger
|
||||
undefined, // customCondensingPrompt
|
||||
undefined, // condensingApiHandler
|
||||
false, // useNativeTools - not using tools in this test
|
||||
)
|
||||
|
||||
// Find the summary message
|
||||
const summaryMessage = result.messages.find((m) => m.isSummary)
|
||||
expect(summaryMessage).toBeDefined()
|
||||
expect(summaryMessage!.role).toBe("assistant")
|
||||
expect(summaryMessage!.isSummary).toBe(true)
|
||||
|
||||
// CRITICAL: Content must be an array with a synthetic reasoning block
|
||||
// This is required for DeepSeek-reasoner which needs reasoning_content on all assistant messages
|
||||
expect(Array.isArray(summaryMessage!.content)).toBe(true)
|
||||
const content = summaryMessage!.content as any[]
|
||||
|
||||
// Should have [synthetic reasoning, text]
|
||||
expect(content).toHaveLength(2)
|
||||
expect(content[0].type).toBe("reasoning")
|
||||
expect(content[0].text).toContain("Condensing conversation context")
|
||||
expect(content[1].type).toBe("text")
|
||||
expect(content[1].text).toBe("Summary: User requested jokes.")
|
||||
|
||||
expect(result.error).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("summarizeConversation with custom settings", () => {
|
||||
|
|
|
|||
|
|
@ -30,22 +30,49 @@ function getToolUseBlocks(message: ApiMessage): Anthropic.Messages.ToolUseBlock[
|
|||
return message.content.filter((block) => block.type === "tool_use") as Anthropic.Messages.ToolUseBlock[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets reasoning blocks from a message's content array.
|
||||
* Task stores reasoning as {type: "reasoning", text: "..."} blocks,
|
||||
* which convertToR1Format and convertToZAiFormat already know how to extract.
|
||||
*/
|
||||
function getReasoningBlocks(message: ApiMessage): Anthropic.Messages.ContentBlockParam[] {
|
||||
if (message.role !== "assistant" || typeof message.content === "string") {
|
||||
return []
|
||||
}
|
||||
// Filter for reasoning blocks and cast to ContentBlockParam (the type field is compatible)
|
||||
return message.content.filter((block) => (block as any).type === "reasoning") as any[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of getKeepMessagesWithToolBlocks
|
||||
*/
|
||||
export type KeepMessagesResult = {
|
||||
keepMessages: ApiMessage[]
|
||||
toolUseBlocksToPreserve: Anthropic.Messages.ToolUseBlock[]
|
||||
// Reasoning blocks from the preceding assistant message, needed for DeepSeek/Z.ai
|
||||
// when tool_use blocks are preserved. Task stores reasoning as {type: "reasoning", text: "..."}
|
||||
// blocks, and convertToR1Format/convertToZAiFormat already extract these.
|
||||
reasoningBlocksToPreserve: Anthropic.Messages.ContentBlockParam[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts tool_use blocks that need to be preserved to match tool_result blocks in keepMessages.
|
||||
* When the first kept message is a user message with tool_result blocks,
|
||||
* we need to find the corresponding tool_use blocks from the preceding assistant message.
|
||||
* These tool_use blocks will be appended to the summary message to maintain proper pairing.
|
||||
*
|
||||
* Also extracts reasoning blocks from the preceding assistant message, which are required
|
||||
* by DeepSeek and Z.ai for interleaved thinking mode. Without these, the API returns a 400 error
|
||||
* "Missing reasoning_content field in the assistant message".
|
||||
* See: https://api-docs.deepseek.com/guides/thinking_mode#tool-calls
|
||||
*
|
||||
* @param messages - The full conversation messages
|
||||
* @param keepCount - The number of messages to keep from the end
|
||||
* @returns Object containing keepMessages and any tool_use blocks to preserve
|
||||
* @returns Object containing keepMessages, tool_use blocks, and reasoning blocks to preserve
|
||||
*/
|
||||
export function getKeepMessagesWithToolBlocks(
|
||||
messages: ApiMessage[],
|
||||
keepCount: number,
|
||||
): { keepMessages: ApiMessage[]; toolUseBlocksToPreserve: Anthropic.Messages.ToolUseBlock[] } {
|
||||
export function getKeepMessagesWithToolBlocks(messages: ApiMessage[], keepCount: number): KeepMessagesResult {
|
||||
if (messages.length <= keepCount) {
|
||||
return { keepMessages: messages, toolUseBlocksToPreserve: [] }
|
||||
return { keepMessages: messages, toolUseBlocksToPreserve: [], reasoningBlocksToPreserve: [] }
|
||||
}
|
||||
|
||||
const startIndex = messages.length - keepCount
|
||||
|
|
@ -59,13 +86,20 @@ export function getKeepMessagesWithToolBlocks(
|
|||
const precedingMessage = messages[precedingIndex]
|
||||
const toolUseBlocks = getToolUseBlocks(precedingMessage)
|
||||
if (toolUseBlocks.length > 0) {
|
||||
// Return the tool_use blocks to be merged into the summary message
|
||||
return { keepMessages, toolUseBlocksToPreserve: toolUseBlocks }
|
||||
// Also extract reasoning blocks for DeepSeek/Z.ai interleaved thinking
|
||||
// Task stores reasoning as {type: "reasoning", text: "..."} content blocks
|
||||
const reasoningBlocks = getReasoningBlocks(precedingMessage)
|
||||
// Return the tool_use blocks and reasoning blocks to be merged into the summary message
|
||||
return {
|
||||
keepMessages,
|
||||
toolUseBlocksToPreserve: toolUseBlocks,
|
||||
reasoningBlocksToPreserve: reasoningBlocks,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { keepMessages, toolUseBlocksToPreserve: [] }
|
||||
return { keepMessages, toolUseBlocksToPreserve: [], reasoningBlocksToPreserve: [] }
|
||||
}
|
||||
|
||||
export const N_MESSAGES_TO_KEEP = 3
|
||||
|
|
@ -168,11 +202,15 @@ export async function summarizeConversation(
|
|||
// Always preserve the first message (which may contain slash command content)
|
||||
const firstMessage = messages[0]
|
||||
|
||||
// Get keepMessages and any tool_use blocks that need to be preserved for tool_result pairing
|
||||
// Only preserve tool_use blocks when using native tools protocol (XML protocol doesn't need them)
|
||||
const { keepMessages, toolUseBlocksToPreserve } = useNativeTools
|
||||
// Get keepMessages and any tool_use/reasoning blocks that need to be preserved for tool_result pairing
|
||||
// Only preserve these blocks when using native tools protocol (XML protocol doesn't need them)
|
||||
const { keepMessages, toolUseBlocksToPreserve, reasoningBlocksToPreserve } = useNativeTools
|
||||
? getKeepMessagesWithToolBlocks(messages, N_MESSAGES_TO_KEEP)
|
||||
: { keepMessages: messages.slice(-N_MESSAGES_TO_KEEP), toolUseBlocksToPreserve: [] }
|
||||
: {
|
||||
keepMessages: messages.slice(-N_MESSAGES_TO_KEEP),
|
||||
toolUseBlocksToPreserve: [],
|
||||
reasoningBlocksToPreserve: [],
|
||||
}
|
||||
|
||||
const keepStartIndex = Math.max(messages.length - N_MESSAGES_TO_KEEP, 0)
|
||||
const includeFirstKeptMessageInSummary = toolUseBlocksToPreserve.length > 0
|
||||
|
|
@ -257,15 +295,39 @@ export async function summarizeConversation(
|
|||
}
|
||||
|
||||
// Build the summary message content
|
||||
// If there are tool_use blocks to preserve (for tool_result pairing), append them to the summary
|
||||
let summaryContent: string | Anthropic.Messages.ContentBlockParam[]
|
||||
// CRITICAL: Always include a reasoning block in the summary for DeepSeek-reasoner compatibility.
|
||||
// DeepSeek-reasoner requires `reasoning_content` on ALL assistant messages, not just those with tool_calls.
|
||||
// Without this, we get: "400 Missing `reasoning_content` field in the assistant message"
|
||||
// See: https://api-docs.deepseek.com/guides/thinking_mode
|
||||
//
|
||||
// The summary content structure is:
|
||||
// 1. Synthetic reasoning block (always present) - for DeepSeek-reasoner compatibility
|
||||
// 2. Any preserved reasoning blocks from the condensed assistant message (if tool_use blocks are preserved)
|
||||
// 3. Text block with the summary
|
||||
// 4. Tool_use blocks (if any need to be preserved for tool_result pairing)
|
||||
|
||||
// Create a synthetic reasoning block that explains the summary
|
||||
// This is minimal but satisfies DeepSeek's requirement for reasoning_content on all assistant messages
|
||||
const syntheticReasoningBlock = {
|
||||
type: "reasoning" as const,
|
||||
text: "Condensing conversation context. The summary below captures the key information from the prior conversation.",
|
||||
}
|
||||
|
||||
const textBlock: Anthropic.Messages.TextBlockParam = { type: "text", text: summary }
|
||||
|
||||
let summaryContent: Anthropic.Messages.ContentBlockParam[]
|
||||
if (toolUseBlocksToPreserve.length > 0) {
|
||||
// Create content array with text block followed by tool_use blocks
|
||||
// Use TextBlockParam which doesn't require citations field
|
||||
const textBlock: Anthropic.Messages.TextBlockParam = { type: "text", text: summary }
|
||||
summaryContent = [textBlock, ...toolUseBlocksToPreserve]
|
||||
// Include: synthetic reasoning, preserved reasoning (if any), summary text, and tool_use blocks
|
||||
summaryContent = [
|
||||
syntheticReasoningBlock as unknown as Anthropic.Messages.ContentBlockParam,
|
||||
...reasoningBlocksToPreserve,
|
||||
textBlock,
|
||||
...toolUseBlocksToPreserve,
|
||||
]
|
||||
} else {
|
||||
summaryContent = summary
|
||||
// Include: synthetic reasoning and summary text
|
||||
// This ensures the summary always has reasoning_content for DeepSeek-reasoner
|
||||
summaryContent = [syntheticReasoningBlock as unknown as Anthropic.Messages.ContentBlockParam, textBlock]
|
||||
}
|
||||
|
||||
// Generate a unique condenseId for this summary
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ export type ApiMessage = Anthropic.MessageParam & {
|
|||
text?: string
|
||||
// For OpenRouter reasoning_details array format (used by Gemini 3, etc.)
|
||||
reasoning_details?: any[]
|
||||
// For DeepSeek/Z.ai interleaved thinking: reasoning_content that must be preserved during tool call sequences
|
||||
// See: https://api-docs.deepseek.com/guides/thinking_mode#tool-calls
|
||||
reasoning_content?: string
|
||||
// For non-destructive condense: unique identifier for summary messages
|
||||
condenseId?: string
|
||||
// For non-destructive condense: points to the condenseId of the summary that replaces this message
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue