mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-07 02:58:15 +00:00
Intelligent Context Condensation v2 (#10873)
This commit is contained in:
parent
d1e74cb3e3
commit
cf5d42e1e1
34 changed files with 2023 additions and 1632 deletions
|
|
@ -27,7 +27,7 @@ describe("Command Mentions", () => {
|
|||
|
||||
// Helper function to call parseMentions with required parameters
|
||||
const callParseMentions = async (text: string) => {
|
||||
const result = await parseMentions(
|
||||
return parseMentions(
|
||||
text,
|
||||
"/test/cwd", // cwd
|
||||
mockUrlContentFetcher, // urlContentFetcher
|
||||
|
|
@ -38,8 +38,6 @@ describe("Command Mentions", () => {
|
|||
50, // maxDiagnosticMessages
|
||||
undefined, // maxReadFileLine
|
||||
)
|
||||
// Return just the text for backward compatibility with existing tests
|
||||
return result.text
|
||||
}
|
||||
|
||||
describe("parseMentions with command support", () => {
|
||||
|
|
@ -56,10 +54,10 @@ describe("Command Mentions", () => {
|
|||
const result = await callParseMentions(input)
|
||||
|
||||
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup")
|
||||
expect(result).toContain('<command name="setup">')
|
||||
expect(result).toContain(commandContent)
|
||||
expect(result).toContain("</command>")
|
||||
expect(result).toContain("Please help me set up the project")
|
||||
expect(result.slashCommandHelp).toContain('<command name="setup">')
|
||||
expect(result.slashCommandHelp).toContain(commandContent)
|
||||
expect(result.slashCommandHelp).toContain("</command>")
|
||||
expect(result.text).toContain("Please help me set up the project")
|
||||
})
|
||||
|
||||
it("should handle multiple commands in message", async () => {
|
||||
|
|
@ -99,10 +97,10 @@ describe("Command Mentions", () => {
|
|||
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup")
|
||||
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "deploy")
|
||||
expect(mockGetCommand).toHaveBeenCalledTimes(2) // Each unique command called once (optimized)
|
||||
expect(result).toContain('<command name="setup">')
|
||||
expect(result).toContain("# Setup Environment")
|
||||
expect(result).toContain('<command name="deploy">')
|
||||
expect(result).toContain("# Deploy Environment")
|
||||
expect(result.slashCommandHelp).toContain('<command name="setup">')
|
||||
expect(result.slashCommandHelp).toContain("# Setup Environment")
|
||||
expect(result.slashCommandHelp).toContain('<command name="deploy">')
|
||||
expect(result.slashCommandHelp).toContain("# Deploy Environment")
|
||||
})
|
||||
|
||||
it("should leave non-existent commands unchanged", async () => {
|
||||
|
|
@ -114,10 +112,10 @@ describe("Command Mentions", () => {
|
|||
|
||||
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "nonexistent")
|
||||
// The command should remain unchanged in the text
|
||||
expect(result).toBe("/nonexistent command")
|
||||
expect(result.text).toBe("/nonexistent command")
|
||||
// Should not contain any command tags
|
||||
expect(result).not.toContain('<command name="nonexistent">')
|
||||
expect(result).not.toContain("Command 'nonexistent' not found")
|
||||
expect(result.slashCommandHelp).toBeUndefined()
|
||||
expect(result.text).not.toContain("Command 'nonexistent' not found")
|
||||
})
|
||||
|
||||
it("should handle command loading errors during existence check", async () => {
|
||||
|
|
@ -129,8 +127,8 @@ describe("Command Mentions", () => {
|
|||
|
||||
// When getCommand throws an error during existence check,
|
||||
// the command is treated as non-existent and left unchanged
|
||||
expect(result).toBe("/error-command test")
|
||||
expect(result).not.toContain('<command name="error-command">')
|
||||
expect(result.text).toBe("/error-command test")
|
||||
expect(result.slashCommandHelp).toBeUndefined()
|
||||
})
|
||||
|
||||
it("should handle command loading errors during processing", async () => {
|
||||
|
|
@ -145,9 +143,9 @@ describe("Command Mentions", () => {
|
|||
const input = "/error-command test"
|
||||
const result = await callParseMentions(input)
|
||||
|
||||
expect(result).toContain('<command name="error-command">')
|
||||
expect(result).toContain("# Error command")
|
||||
expect(result).toContain("</command>")
|
||||
expect(result.slashCommandHelp).toContain('<command name="error-command">')
|
||||
expect(result.slashCommandHelp).toContain("# Error command")
|
||||
expect(result.slashCommandHelp).toContain("</command>")
|
||||
})
|
||||
|
||||
it("should handle command names with hyphens and underscores at start", async () => {
|
||||
|
|
@ -162,8 +160,8 @@ describe("Command Mentions", () => {
|
|||
const result = await callParseMentions(input)
|
||||
|
||||
expect(mockGetCommand).toHaveBeenCalledWith("/test/cwd", "setup-dev")
|
||||
expect(result).toContain('<command name="setup-dev">')
|
||||
expect(result).toContain("# Dev setup")
|
||||
expect(result.slashCommandHelp).toContain('<command name="setup-dev">')
|
||||
expect(result.slashCommandHelp).toContain("# Dev setup")
|
||||
})
|
||||
|
||||
it("should preserve command content formatting", async () => {
|
||||
|
|
@ -192,13 +190,13 @@ npm install
|
|||
const input = "/complex command"
|
||||
const result = await callParseMentions(input)
|
||||
|
||||
expect(result).toContain('<command name="complex">')
|
||||
expect(result).toContain("# Complex Command")
|
||||
expect(result).toContain("```bash")
|
||||
expect(result).toContain("npm install")
|
||||
expect(result).toContain("- Check file1.js")
|
||||
expect(result).toContain("> **Note**: This is important!")
|
||||
expect(result).toContain("</command>")
|
||||
expect(result.slashCommandHelp).toContain('<command name="complex">')
|
||||
expect(result.slashCommandHelp).toContain("# Complex Command")
|
||||
expect(result.slashCommandHelp).toContain("```bash")
|
||||
expect(result.slashCommandHelp).toContain("npm install")
|
||||
expect(result.slashCommandHelp).toContain("- Check file1.js")
|
||||
expect(result.slashCommandHelp).toContain("> **Note**: This is important!")
|
||||
expect(result.slashCommandHelp).toContain("</command>")
|
||||
})
|
||||
|
||||
it("should handle empty command content", async () => {
|
||||
|
|
@ -212,8 +210,8 @@ npm install
|
|||
const input = "/empty command"
|
||||
const result = await callParseMentions(input)
|
||||
|
||||
expect(result).toContain('<command name="empty">')
|
||||
expect(result).toContain("</command>")
|
||||
expect(result.slashCommandHelp).toContain('<command name="empty">')
|
||||
expect(result.slashCommandHelp).toContain("</command>")
|
||||
// Should still include the command tags even with empty content
|
||||
})
|
||||
})
|
||||
|
|
@ -295,7 +293,7 @@ npm install
|
|||
const input = "/setup the project"
|
||||
const result = await callParseMentions(input)
|
||||
|
||||
expect(result).toContain("Command 'setup' (see below for command content)")
|
||||
expect(result.text).toContain("Command 'setup' (see below for command content)")
|
||||
})
|
||||
|
||||
it("should leave non-existent command mentions unchanged", async () => {
|
||||
|
|
@ -304,7 +302,7 @@ npm install
|
|||
const input = "/nonexistent the project"
|
||||
const result = await callParseMentions(input)
|
||||
|
||||
expect(result).toBe("/nonexistent the project")
|
||||
expect(result.text).toBe("/nonexistent the project")
|
||||
})
|
||||
|
||||
it("should process multiple commands in message", async () => {
|
||||
|
|
@ -325,8 +323,8 @@ npm install
|
|||
const input = "/setup the project\nThen /deploy later"
|
||||
const result = await callParseMentions(input)
|
||||
|
||||
expect(result).toContain("Command 'setup' (see below for command content)")
|
||||
expect(result).toContain("Command 'deploy' (see below for command content)")
|
||||
expect(result.text).toContain("Command 'setup' (see below for command content)")
|
||||
expect(result.text).toContain("Command 'deploy' (see below for command content)")
|
||||
})
|
||||
|
||||
it("should match commands anywhere with proper word boundaries", async () => {
|
||||
|
|
@ -340,22 +338,22 @@ npm install
|
|||
// At the beginning - should match
|
||||
let input = "/build the project"
|
||||
let result = await callParseMentions(input)
|
||||
expect(result).toContain("Command 'build'")
|
||||
expect(result.text).toContain("Command 'build'")
|
||||
|
||||
// After space - should match
|
||||
input = "Please /build and test"
|
||||
result = await callParseMentions(input)
|
||||
expect(result).toContain("Command 'build'")
|
||||
expect(result.text).toContain("Command 'build'")
|
||||
|
||||
// At the end - should match
|
||||
input = "Run the /build"
|
||||
result = await callParseMentions(input)
|
||||
expect(result).toContain("Command 'build'")
|
||||
expect(result.text).toContain("Command 'build'")
|
||||
|
||||
// At start of new line - should match
|
||||
input = "Some text\n/build the project"
|
||||
result = await callParseMentions(input)
|
||||
expect(result).toContain("Command 'build'")
|
||||
expect(result.text).toContain("Command 'build'")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
summarizeConversation,
|
||||
getMessagesSinceLastSummary,
|
||||
getEffectiveApiHistory,
|
||||
N_MESSAGES_TO_KEEP,
|
||||
extractCommandBlocks,
|
||||
} from "../index"
|
||||
|
||||
// Create a mock ApiHandler for testing
|
||||
|
|
@ -63,8 +63,67 @@ describe("Condense", () => {
|
|||
}
|
||||
})
|
||||
|
||||
describe("extractCommandBlocks", () => {
|
||||
it("should extract command blocks from string content", () => {
|
||||
const message: ApiMessage = {
|
||||
role: "user",
|
||||
content: 'Some text <command name="prr">/prr #123</command> more text',
|
||||
}
|
||||
|
||||
const result = extractCommandBlocks(message)
|
||||
expect(result).toBe('<command name="prr">/prr #123</command>')
|
||||
})
|
||||
|
||||
it("should extract multiple command blocks", () => {
|
||||
const message: ApiMessage = {
|
||||
role: "user",
|
||||
content: '<command name="prr">/prr #123</command> text <command name="mode">/mode code</command>',
|
||||
}
|
||||
|
||||
const result = extractCommandBlocks(message)
|
||||
expect(result).toBe('<command name="prr">/prr #123</command>\n<command name="mode">/mode code</command>')
|
||||
})
|
||||
|
||||
it("should extract command blocks from array content", () => {
|
||||
const message: ApiMessage = {
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Some user text" },
|
||||
{ type: "text", text: '<command name="prr">Help content</command>' },
|
||||
],
|
||||
}
|
||||
|
||||
const result = extractCommandBlocks(message)
|
||||
expect(result).toBe('<command name="prr">Help content</command>')
|
||||
})
|
||||
|
||||
it("should return empty string when no command blocks found", () => {
|
||||
const message: ApiMessage = {
|
||||
role: "user",
|
||||
content: "Just regular text without commands",
|
||||
}
|
||||
|
||||
const result = extractCommandBlocks(message)
|
||||
expect(result).toBe("")
|
||||
})
|
||||
|
||||
it("should handle multiline command blocks", () => {
|
||||
const message: ApiMessage = {
|
||||
role: "user",
|
||||
content: `<command name="prr">
|
||||
Line 1
|
||||
Line 2
|
||||
</command>`,
|
||||
}
|
||||
|
||||
const result = extractCommandBlocks(message)
|
||||
expect(result).toContain("Line 1")
|
||||
expect(result).toContain("Line 2")
|
||||
})
|
||||
})
|
||||
|
||||
describe("summarizeConversation", () => {
|
||||
it("should preserve the first message when summarizing", async () => {
|
||||
it("should create a summary message with role user (fresh start model)", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "First message with /prr command content" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
|
|
@ -79,57 +138,75 @@ describe("Condense", () => {
|
|||
|
||||
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
|
||||
|
||||
// Verify the first message is preserved
|
||||
expect(result.messages[0]).toEqual(messages[0])
|
||||
expect(result.messages[0].content).toBe("First message with /prr command content")
|
||||
|
||||
// Verify we have a summary message
|
||||
// Verify we have a summary message with role "user" (fresh start model)
|
||||
const summaryMessage = result.messages.find((msg) => msg.isSummary)
|
||||
expect(summaryMessage).toBeTruthy()
|
||||
// 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",
|
||||
})
|
||||
expect(summaryMessage!.role).toBe("user")
|
||||
expect(Array.isArray(summaryMessage!.content)).toBe(true)
|
||||
const contentArray = summaryMessage!.content as any[]
|
||||
expect(contentArray.some((b) => b.type === "text")).toBe(true)
|
||||
// Should NOT have reasoning blocks (no longer needed for user messages)
|
||||
expect(contentArray.some((b) => b.type === "reasoning")).toBe(false)
|
||||
|
||||
// With non-destructive condensing, all messages are retained (tagged but not deleted)
|
||||
// Use getEffectiveApiHistory to verify the effective view matches the old behavior
|
||||
expect(result.messages.length).toBe(messages.length + 1) // All original messages + summary
|
||||
// Fresh start model: effective history should only contain the summary
|
||||
const effectiveHistory = getEffectiveApiHistory(result.messages)
|
||||
expect(effectiveHistory.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // first + summary + last N
|
||||
|
||||
// Verify the last N messages are preserved (same messages by reference)
|
||||
const lastMessages = result.messages.slice(-N_MESSAGES_TO_KEEP)
|
||||
expect(lastMessages).toEqual(messages.slice(-N_MESSAGES_TO_KEEP))
|
||||
expect(effectiveHistory.length).toBe(1)
|
||||
expect(effectiveHistory[0].isSummary).toBe(true)
|
||||
expect(effectiveHistory[0].role).toBe("user")
|
||||
})
|
||||
|
||||
it("should preserve slash command content in the first message", async () => {
|
||||
const slashCommandContent = "/prr #123 - Fix authentication bug"
|
||||
it("should tag ALL messages with condenseParent", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: slashCommandContent },
|
||||
{ role: "assistant", content: "I'll help you fix that authentication bug" },
|
||||
{ role: "user", content: "The issue is with JWT tokens" },
|
||||
{ role: "assistant", content: "Let me examine the JWT implementation" },
|
||||
{ role: "user", content: "It's failing on refresh" },
|
||||
{ role: "assistant", content: "I found the issue" },
|
||||
{ role: "user", content: "Great, can you fix it?" },
|
||||
{ role: "assistant", content: "Here's the fix" },
|
||||
{ role: "user", content: "Thanks!" },
|
||||
{ role: "user", content: "First message with /prr command content" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
{ role: "assistant", content: "Fourth message" },
|
||||
{ role: "user", content: "Fifth message" },
|
||||
]
|
||||
|
||||
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
|
||||
|
||||
// The first message with slash command should be intact
|
||||
expect(result.messages[0].content).toBe(slashCommandContent)
|
||||
expect(result.messages[0]).toEqual(messages[0])
|
||||
// All original messages should be tagged with condenseParent
|
||||
const taggedMessages = result.messages.filter((msg) => !msg.isSummary)
|
||||
expect(taggedMessages.length).toBe(messages.length)
|
||||
for (const msg of taggedMessages) {
|
||||
expect(msg.condenseParent).toBeDefined()
|
||||
}
|
||||
})
|
||||
|
||||
it("should preserve <command> blocks in the summary", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
{
|
||||
role: "user",
|
||||
content: [
|
||||
{ type: "text", text: "Some user text" },
|
||||
{ type: "text", text: '<command name="prr">Help content</command>' },
|
||||
],
|
||||
},
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
{ role: "assistant", content: "Fourth message" },
|
||||
{ role: "user", content: "Fifth message" },
|
||||
{ role: "assistant", content: "Sixth message" },
|
||||
{ role: "user", content: "Seventh message" },
|
||||
{ role: "assistant", content: "Eighth message" },
|
||||
{ role: "user", content: "Ninth message" },
|
||||
]
|
||||
|
||||
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
|
||||
|
||||
const summaryMessage = result.messages.find((msg) => msg.isSummary)
|
||||
expect(summaryMessage).toBeTruthy()
|
||||
|
||||
const contentArray = summaryMessage!.content as any[]
|
||||
// Summary content is split into separate text blocks:
|
||||
// - First block: "## Conversation Summary\n..."
|
||||
// - Second block: "<system-reminder>..." with command blocks
|
||||
expect(contentArray).toHaveLength(2)
|
||||
expect(contentArray[0].text).toContain("## Conversation Summary")
|
||||
expect(contentArray[1].text).toContain('<command name="prr">')
|
||||
expect(contentArray[1].text).toContain("<system-reminder>")
|
||||
expect(contentArray[1].text).toContain("Active Workflows")
|
||||
})
|
||||
|
||||
it("should handle complex first message content", async () => {
|
||||
|
|
@ -152,41 +229,33 @@ describe("Condense", () => {
|
|||
|
||||
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
|
||||
|
||||
// The first message with complex content should be preserved
|
||||
expect(result.messages[0].content).toEqual(complexContent)
|
||||
expect(result.messages[0]).toEqual(messages[0])
|
||||
// Effective history should contain only the summary (fresh start)
|
||||
const effectiveHistory = getEffectiveApiHistory(result.messages)
|
||||
expect(effectiveHistory).toHaveLength(1)
|
||||
expect(effectiveHistory[0].isSummary).toBe(true)
|
||||
expect(effectiveHistory[0].role).toBe("user")
|
||||
})
|
||||
|
||||
it("should return error when not enough messages to summarize", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "First message with /command" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "user", content: "Third message" },
|
||||
{ role: "assistant", content: "Fourth message" },
|
||||
]
|
||||
const messages: ApiMessage[] = [{ role: "user", content: "Only one message" }]
|
||||
|
||||
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
|
||||
|
||||
// Should return an error since we have only 4 messages (first + 3 to keep)
|
||||
// Should return an error since we have only 1 message
|
||||
expect(result.error).toBeDefined()
|
||||
expect(result.messages).toEqual(messages) // Original messages unchanged
|
||||
expect(result.summary).toBe("")
|
||||
})
|
||||
|
||||
it("should not summarize messages that already contain a recent summary", async () => {
|
||||
it("should not summarize messages that already contain a recent summary with no new messages", async () => {
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "First message with /command" },
|
||||
{ role: "assistant", content: "Old message" },
|
||||
{ role: "user", content: "Message before summary" },
|
||||
{ role: "assistant", content: "Response" },
|
||||
{ role: "user", content: "Another message" },
|
||||
{ role: "assistant", content: "Previous summary", isSummary: true }, // Summary in last N messages
|
||||
{ role: "user", content: "Final message" },
|
||||
{ role: "assistant", content: "Previous summary", isSummary: true },
|
||||
]
|
||||
|
||||
const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
|
||||
|
||||
// Should return an error due to recent summary in last N messages
|
||||
// Should return an error due to recent summary with no substantial messages after
|
||||
expect(result.error).toBeDefined()
|
||||
expect(result.messages).toEqual(messages)
|
||||
expect(result.summary).toBe("")
|
||||
|
|
@ -225,6 +294,81 @@ describe("Condense", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("getEffectiveApiHistory", () => {
|
||||
it("should return only summary when summary exists (fresh start)", () => {
|
||||
const condenseId = "test-condense-id"
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "First", condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Second", condenseParent: condenseId },
|
||||
{ role: "user", content: "Third", condenseParent: condenseId },
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Summary content" }],
|
||||
isSummary: true,
|
||||
condenseId,
|
||||
},
|
||||
]
|
||||
|
||||
const result = getEffectiveApiHistory(messages)
|
||||
|
||||
expect(result).toHaveLength(1)
|
||||
expect(result[0].isSummary).toBe(true)
|
||||
})
|
||||
|
||||
it("should include messages after summary in fresh start model", () => {
|
||||
const condenseId = "test-condense-id"
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "First", condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Second", condenseParent: condenseId },
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Summary content" }],
|
||||
isSummary: true,
|
||||
condenseId,
|
||||
},
|
||||
{ role: "assistant", content: "New response after summary" },
|
||||
{ role: "user", content: "New user message" },
|
||||
]
|
||||
|
||||
const result = getEffectiveApiHistory(messages)
|
||||
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[0].isSummary).toBe(true)
|
||||
expect(result[1].content).toBe("New response after summary")
|
||||
expect(result[2].content).toBe("New user message")
|
||||
})
|
||||
|
||||
it("should return all messages when no summary exists", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "First" },
|
||||
{ role: "assistant", content: "Second" },
|
||||
{ role: "user", content: "Third" },
|
||||
]
|
||||
|
||||
const result = getEffectiveApiHistory(messages)
|
||||
|
||||
expect(result).toEqual(messages)
|
||||
})
|
||||
|
||||
it("should restore messages when summary is deleted (rewind)", () => {
|
||||
// After rewind, summary is deleted but condenseParent tags remain as orphans
|
||||
// The cleanupAfterTruncation function would normally clear these,
|
||||
// but even without cleanup, getEffectiveApiHistory should handle orphaned tags
|
||||
const orphanedCondenseId = "deleted-summary-id"
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "First", condenseParent: orphanedCondenseId },
|
||||
{ role: "assistant", content: "Second", condenseParent: orphanedCondenseId },
|
||||
{ role: "user", content: "Third", condenseParent: orphanedCondenseId },
|
||||
// Summary was deleted - no isSummary message exists
|
||||
]
|
||||
|
||||
const result = getEffectiveApiHistory(messages)
|
||||
|
||||
// With no summary, all messages should be included (orphaned condenseParent is ignored)
|
||||
expect(result).toHaveLength(3)
|
||||
})
|
||||
})
|
||||
|
||||
describe("getMessagesSinceLastSummary", () => {
|
||||
it("should return all messages when no summary exists", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
|
|
@ -241,39 +385,48 @@ describe("Condense", () => {
|
|||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "Second message" },
|
||||
{ role: "assistant", content: "Summary content", isSummary: true },
|
||||
{ role: "user", content: "Message after summary" },
|
||||
{ role: "assistant", content: "Final message" },
|
||||
{ role: "user", content: "Summary content", isSummary: true },
|
||||
{ role: "assistant", content: "Message after summary" },
|
||||
{ role: "user", content: "Final message" },
|
||||
]
|
||||
|
||||
const result = getMessagesSinceLastSummary(messages)
|
||||
|
||||
// Should include the original first user message for context preservation, the summary, and messages after
|
||||
expect(result[0].role).toBe("user")
|
||||
expect(result[0].content).toBe("First message") // Preserves original first message
|
||||
expect(result[1]).toEqual(messages[2]) // The summary
|
||||
expect(result[2]).toEqual(messages[3])
|
||||
expect(result[3]).toEqual(messages[4])
|
||||
expect(result[0]).toEqual(messages[2]) // The summary
|
||||
expect(result[1]).toEqual(messages[3])
|
||||
expect(result[2]).toEqual(messages[4])
|
||||
})
|
||||
|
||||
it("should handle multiple summaries and return from the last one", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "First message" },
|
||||
{ role: "assistant", content: "First summary", isSummary: true },
|
||||
{ role: "user", content: "Middle message" },
|
||||
{ role: "assistant", content: "Second summary", isSummary: true },
|
||||
{ role: "user", content: "Recent message" },
|
||||
{ role: "assistant", content: "Final message" },
|
||||
{ role: "user", content: "First summary", isSummary: true },
|
||||
{ role: "assistant", content: "Middle message" },
|
||||
{ role: "user", content: "Second summary", isSummary: true },
|
||||
{ role: "assistant", content: "Recent message" },
|
||||
{ role: "user", content: "Final message" },
|
||||
]
|
||||
|
||||
const result = getMessagesSinceLastSummary(messages)
|
||||
|
||||
// Should only include from the last summary with original first message preserved
|
||||
expect(result[0].role).toBe("user")
|
||||
expect(result[0].content).toBe("First message") // Preserves original first message
|
||||
expect(result[1]).toEqual(messages[3]) // Second summary
|
||||
expect(result[2]).toEqual(messages[4])
|
||||
expect(result[3]).toEqual(messages[5])
|
||||
expect(result[0]).toEqual(messages[3]) // Second summary
|
||||
expect(result[1]).toEqual(messages[4])
|
||||
expect(result[2]).toEqual(messages[5])
|
||||
})
|
||||
|
||||
it("should prepend first user message when summary starts with assistant", () => {
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "Original first message" },
|
||||
{ role: "assistant", content: "Summary content", isSummary: true },
|
||||
{ role: "user", content: "After summary" },
|
||||
]
|
||||
|
||||
const result = getMessagesSinceLastSummary(messages)
|
||||
|
||||
// Should prepend original first message for Bedrock compatibility
|
||||
expect(result[0]).toEqual(messages[0]) // Original first user message
|
||||
expect(result[1]).toEqual(messages[1]) // The summary
|
||||
expect(result[2]).toEqual(messages[2])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -22,25 +22,25 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
})
|
||||
|
||||
describe("getEffectiveApiHistory", () => {
|
||||
it("should filter out messages tagged with condenseParent", () => {
|
||||
it("should return summary and messages after summary (fresh start model)", () => {
|
||||
const condenseId = "summary-123"
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "First message", ts: 1 },
|
||||
{ role: "user", content: "First message", ts: 1, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "First response", ts: 2, condenseParent: condenseId },
|
||||
{ role: "user", content: "Second message", ts: 3, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Summary", ts: 4, isSummary: true, condenseId },
|
||||
{ role: "user", content: "Third message", ts: 5 },
|
||||
{ role: "assistant", content: "Third response", ts: 6 },
|
||||
{ role: "user", content: "Summary", ts: 4, isSummary: true, condenseId },
|
||||
// Messages after summary are included even if they have condenseParent
|
||||
{ role: "user", content: "Third message", ts: 5, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Third response", ts: 6, condenseParent: condenseId },
|
||||
]
|
||||
|
||||
const effective = getEffectiveApiHistory(messages)
|
||||
|
||||
// Effective history should be: first message, summary, third message, third response
|
||||
expect(effective.length).toBe(4)
|
||||
expect(effective[0].content).toBe("First message")
|
||||
expect(effective[1].isSummary).toBe(true)
|
||||
expect(effective[2].content).toBe("Third message")
|
||||
expect(effective[3].content).toBe("Third response")
|
||||
// Fresh start model: summary + all messages after it
|
||||
expect(effective.length).toBe(3)
|
||||
expect(effective[0].isSummary).toBe(true)
|
||||
expect(effective[1].content).toBe("Third message")
|
||||
expect(effective[2].content).toBe("Third response")
|
||||
})
|
||||
|
||||
it("should include messages without condenseParent", () => {
|
||||
|
|
@ -131,44 +131,32 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
it("should reactivate condensed messages when their summary is deleted via truncation", () => {
|
||||
const condenseId = "summary-abc"
|
||||
|
||||
// Simulate a conversation after condensing
|
||||
// Simulate a conversation after condensing (all prior messages tagged)
|
||||
const fullHistory: ApiMessage[] = [
|
||||
{ role: "user", content: "Initial task", ts: 1 },
|
||||
{ role: "user", content: "Initial task", ts: 1, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Working on it", ts: 2, condenseParent: condenseId },
|
||||
{ role: "user", content: "Continue", ts: 3, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Summary of work so far", ts: 4, isSummary: true, condenseId },
|
||||
{ role: "user", content: "Now do this", ts: 5 },
|
||||
{ role: "assistant", content: "Done", ts: 6 },
|
||||
{ role: "user", content: "And this", ts: 7 },
|
||||
{ role: "assistant", content: "Also done", ts: 8 },
|
||||
{ role: "user", content: "Summary of work so far", ts: 4, isSummary: true, condenseId },
|
||||
]
|
||||
|
||||
// Verify effective history before truncation
|
||||
const effectiveBefore = getEffectiveApiHistory(fullHistory)
|
||||
// Should be: first message, summary, last 4 messages
|
||||
expect(effectiveBefore.length).toBe(6)
|
||||
// Should be: summary only
|
||||
expect(effectiveBefore.length).toBe(1)
|
||||
|
||||
// Simulate rewind: user truncates back to message ts=4 (keeping 0-3)
|
||||
const truncatedHistory = fullHistory.slice(0, 4) // Keep first, condensed1, condensed2, summary
|
||||
// Simulate rewind: delete the summary message
|
||||
const withoutSummary = fullHistory.filter((m) => !m.isSummary)
|
||||
const cleanedAfterDeletingSummary = cleanupAfterTruncation(withoutSummary)
|
||||
for (const msg of cleanedAfterDeletingSummary) {
|
||||
expect(msg.condenseParent).toBeUndefined()
|
||||
}
|
||||
|
||||
// After truncation, the summary is still there, so condensed messages remain condensed
|
||||
const cleanedAfterKeepingSummary = cleanupAfterTruncation(truncatedHistory)
|
||||
expect(cleanedAfterKeepingSummary[1].condenseParent).toBe(condenseId)
|
||||
expect(cleanedAfterKeepingSummary[2].condenseParent).toBe(condenseId)
|
||||
|
||||
// Now simulate a more aggressive rewind: delete back to message ts=2
|
||||
const aggressiveTruncate = fullHistory.slice(0, 2) // Keep only first message and first response
|
||||
|
||||
// The condensed messages should now be reactivated since summary is gone
|
||||
const cleanedAfterDeletingSummary = cleanupAfterTruncation(aggressiveTruncate)
|
||||
expect(cleanedAfterDeletingSummary[1].condenseParent).toBeUndefined()
|
||||
|
||||
// Verify effective history after cleanup
|
||||
// Verify effective history after cleanup: all messages should be visible now
|
||||
const effectiveAfterCleanup = getEffectiveApiHistory(cleanedAfterDeletingSummary)
|
||||
// Now both messages should be active (no condensed filtering)
|
||||
expect(effectiveAfterCleanup.length).toBe(2)
|
||||
expect(effectiveAfterCleanup.length).toBe(3)
|
||||
expect(effectiveAfterCleanup[0].content).toBe("Initial task")
|
||||
expect(effectiveAfterCleanup[1].content).toBe("Working on it")
|
||||
expect(effectiveAfterCleanup[2].content).toBe("Continue")
|
||||
})
|
||||
|
||||
it("should properly restore context after rewind when summary was deleted", () => {
|
||||
|
|
@ -206,10 +194,11 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
expect(effectiveAfter.length).toBe(5) // All messages visible
|
||||
})
|
||||
|
||||
it("should hide condensed messages when their summary still exists", () => {
|
||||
it("should hide condensed messages when their summary still exists (fresh start)", () => {
|
||||
const condenseId = "summary-exists"
|
||||
|
||||
// Scenario: Messages were condensed and summary exists - condensed messages should be hidden
|
||||
// Scenario: Messages were condensed and summary exists - fresh start model returns
|
||||
// only the summary and messages after it, NOT messages before the summary
|
||||
const messages: ApiMessage[] = [
|
||||
{ role: "user", content: "Start", ts: 1 },
|
||||
{ role: "assistant", content: "Response 1", ts: 2, condenseParent: condenseId },
|
||||
|
|
@ -218,12 +207,13 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
{ role: "user", content: "After summary", ts: 5 },
|
||||
]
|
||||
|
||||
// Effective history should hide condensed messages since summary exists
|
||||
// Fresh start model: effective history is summary + messages after it
|
||||
// "Start" is NOT included because it's before the summary
|
||||
const effective = getEffectiveApiHistory(messages)
|
||||
expect(effective.length).toBe(3) // Start, Summary, After summary
|
||||
expect(effective[0].content).toBe("Start")
|
||||
expect(effective[1].content).toBe("Summary")
|
||||
expect(effective[2].content).toBe("After summary")
|
||||
expect(effective.length).toBe(2) // Summary, After summary (NOT Start)
|
||||
expect(effective[0].content).toBe("Summary")
|
||||
expect(effective[0].isSummary).toBe(true)
|
||||
expect(effective[1].content).toBe("After summary")
|
||||
|
||||
// cleanupAfterTruncation should NOT clear condenseParent since summary exists
|
||||
const cleaned = cleanupAfterTruncation(messages)
|
||||
|
|
@ -315,8 +305,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
/**
|
||||
* These tests verify that the correct user and assistant messages are preserved
|
||||
* and sent to the LLM after condense operations. With N_MESSAGES_TO_KEEP = 3,
|
||||
* condense should always preserve:
|
||||
* - The first message (never condensed)
|
||||
* condense should always preserve (for effective history):
|
||||
* - The active summary
|
||||
* - The last 3 kept messages
|
||||
*/
|
||||
|
|
@ -332,7 +321,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
// - summary inserted with ts = msg8.ts - 1
|
||||
// - msg8, msg9, msg10 kept
|
||||
const storageAfterCondense: ApiMessage[] = [
|
||||
{ role: "user", content: "Task: Build a feature", ts: 100 },
|
||||
{ role: "user", content: "Task: Build a feature", ts: 100, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "I'll help with that", ts: 200, condenseParent: condenseId },
|
||||
{ role: "user", content: "Start with the API", ts: 300, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Creating API endpoints", ts: 400, condenseParent: condenseId },
|
||||
|
|
@ -355,28 +344,24 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
|
||||
const effective = getEffectiveApiHistory(storageAfterCondense)
|
||||
|
||||
// Should send exactly 5 messages to LLM:
|
||||
// 1. First message (user) - preserved
|
||||
// 2. Summary (assistant)
|
||||
// 3-5. Last 3 kept messages
|
||||
expect(effective.length).toBe(5)
|
||||
// Should send exactly 4 messages to LLM:
|
||||
// 1. Summary (assistant)
|
||||
// 2-4. Last 3 kept messages
|
||||
expect(effective.length).toBe(4)
|
||||
|
||||
// Verify exact order and content
|
||||
expect(effective[0].role).toBe("user")
|
||||
expect(effective[0].content).toBe("Task: Build a feature")
|
||||
expect(effective[0].role).toBe("assistant")
|
||||
expect(effective[0].isSummary).toBe(true)
|
||||
expect(effective[0].content).toBe("Summary: Built API with validation, working on tests")
|
||||
|
||||
expect(effective[1].role).toBe("assistant")
|
||||
expect(effective[1].isSummary).toBe(true)
|
||||
expect(effective[1].content).toBe("Summary: Built API with validation, working on tests")
|
||||
expect(effective[1].content).toBe("Writing unit tests now")
|
||||
|
||||
expect(effective[2].role).toBe("assistant")
|
||||
expect(effective[2].content).toBe("Writing unit tests now")
|
||||
expect(effective[2].role).toBe("user")
|
||||
expect(effective[2].content).toBe("Include edge cases")
|
||||
|
||||
expect(effective[3].role).toBe("user")
|
||||
expect(effective[3].content).toBe("Include edge cases")
|
||||
|
||||
expect(effective[4].role).toBe("assistant")
|
||||
expect(effective[4].content).toBe("Added edge case tests")
|
||||
expect(effective[3].role).toBe("assistant")
|
||||
expect(effective[3].content).toBe("Added edge case tests")
|
||||
|
||||
// Verify condensed messages are NOT in effective history
|
||||
const condensedContents = ["I'll help with that", "Start with the API", "Creating API endpoints"]
|
||||
|
|
@ -396,8 +381,8 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
//
|
||||
// Storage after double condense:
|
||||
const storageAfterDoubleCondense: ApiMessage[] = [
|
||||
// First message - never condensed
|
||||
{ role: "user", content: "Initial task: Build a full app", ts: 100 },
|
||||
// First message - condensed during the first condense
|
||||
{ role: "user", content: "Initial task: Build a full app", ts: 100, condenseParent: condenseId1 },
|
||||
|
||||
// Messages from first condense (tagged with condenseId1)
|
||||
{ role: "assistant", content: "Starting the project", ts: 200, condenseParent: condenseId1 },
|
||||
|
|
@ -446,29 +431,25 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
|
||||
const effective = getEffectiveApiHistory(storageAfterDoubleCondense)
|
||||
|
||||
// Should send exactly 5 messages to LLM:
|
||||
// 1. First message (user) - preserved
|
||||
// 2. Summary2 (assistant) - the ACTIVE summary
|
||||
// 3-5. Last 3 kept messages
|
||||
expect(effective.length).toBe(5)
|
||||
// Should send exactly 4 messages to LLM:
|
||||
// 1. Summary2 (assistant) - the ACTIVE summary
|
||||
// 2-4. Last 3 kept messages
|
||||
expect(effective.length).toBe(4)
|
||||
|
||||
// Verify exact order and content
|
||||
expect(effective[0].role).toBe("user")
|
||||
expect(effective[0].content).toBe("Initial task: Build a full app")
|
||||
expect(effective[0].role).toBe("assistant")
|
||||
expect(effective[0].isSummary).toBe(true)
|
||||
expect(effective[0].condenseId).toBe(condenseId2) // Must be the SECOND summary
|
||||
expect(effective[0].content).toContain("Summary2")
|
||||
|
||||
expect(effective[1].role).toBe("assistant")
|
||||
expect(effective[1].isSummary).toBe(true)
|
||||
expect(effective[1].condenseId).toBe(condenseId2) // Must be the SECOND summary
|
||||
expect(effective[1].content).toContain("Summary2")
|
||||
expect(effective[1].content).toBe("Writing integration tests")
|
||||
|
||||
expect(effective[2].role).toBe("assistant")
|
||||
expect(effective[2].content).toBe("Writing integration tests")
|
||||
expect(effective[2].role).toBe("user")
|
||||
expect(effective[2].content).toBe("Test the auth flow")
|
||||
|
||||
expect(effective[3].role).toBe("user")
|
||||
expect(effective[3].content).toBe("Test the auth flow")
|
||||
|
||||
expect(effective[4].role).toBe("assistant")
|
||||
expect(effective[4].content).toBe("Auth tests passing")
|
||||
expect(effective[3].role).toBe("assistant")
|
||||
expect(effective[3].content).toBe("Auth tests passing")
|
||||
|
||||
// Verify Summary1 is NOT in effective history (it's tagged with condenseParent)
|
||||
const summary1 = effective.find((m) => m.content?.toString().includes("Summary1"))
|
||||
|
|
@ -493,7 +474,7 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
// Verify that after condense, the effective history maintains proper
|
||||
// user/assistant message alternation (important for API compatibility)
|
||||
const storage: ApiMessage[] = [
|
||||
{ role: "user", content: "Start task", ts: 100 },
|
||||
{ role: "user", content: "Start task", ts: 100, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Response 1", ts: 200, condenseParent: condenseId },
|
||||
{ role: "user", content: "Continue", ts: 300, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Summary text", ts: 399, isSummary: true, condenseId },
|
||||
|
|
@ -505,22 +486,21 @@ describe("Rewind After Condense - Issue #8295", () => {
|
|||
|
||||
const effective = getEffectiveApiHistory(storage)
|
||||
|
||||
// Verify the sequence: user, assistant(summary), assistant, user, assistant
|
||||
// Verify the sequence: assistant(summary), assistant, user, assistant
|
||||
// Note: Having two assistant messages in a row (summary + next response) is valid
|
||||
// because the summary replaces what would have been multiple messages
|
||||
expect(effective[0].role).toBe("user")
|
||||
expect(effective[0].role).toBe("assistant")
|
||||
expect(effective[0].isSummary).toBe(true)
|
||||
expect(effective[1].role).toBe("assistant")
|
||||
expect(effective[1].isSummary).toBe(true)
|
||||
expect(effective[2].role).toBe("assistant")
|
||||
expect(effective[3].role).toBe("user")
|
||||
expect(effective[4].role).toBe("assistant")
|
||||
expect(effective[2].role).toBe("user")
|
||||
expect(effective[3].role).toBe("assistant")
|
||||
})
|
||||
|
||||
it("should preserve timestamps in chronological order in effective history", () => {
|
||||
const condenseId = "summary-timestamps"
|
||||
|
||||
const storage: ApiMessage[] = [
|
||||
{ role: "user", content: "First", ts: 100 },
|
||||
{ role: "user", content: "First", ts: 100, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Condensed", ts: 200, condenseParent: condenseId },
|
||||
{ role: "assistant", content: "Summary", ts: 299, isSummary: true, condenseId },
|
||||
{ role: "user", content: "Kept 1", ts: 300 },
|
||||
|
|
|
|||
|
|
@ -4,157 +4,114 @@ import crypto from "crypto"
|
|||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
||||
import { t } from "../../i18n"
|
||||
import { ApiHandler } from "../../api"
|
||||
import { ApiHandler, ApiHandlerCreateMessageMetadata } from "../../api"
|
||||
import { ApiMessage } from "../task-persistence/apiMessages"
|
||||
import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning"
|
||||
import { findLast } from "../../shared/array"
|
||||
import { supportPrompt } from "../../shared/support-prompt"
|
||||
|
||||
/**
|
||||
* Checks if a message contains tool_result blocks.
|
||||
* User messages with tool_result blocks require corresponding tool_use blocks from the previous assistant turn.
|
||||
*/
|
||||
function hasToolResultBlocks(message: ApiMessage): boolean {
|
||||
if (message.role !== "user" || typeof message.content === "string") {
|
||||
return false
|
||||
}
|
||||
return message.content.some((block) => block.type === "tool_result")
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tool_use blocks from a message.
|
||||
*/
|
||||
function getToolUseBlocks(message: ApiMessage): Anthropic.Messages.ToolUseBlock[] {
|
||||
if (message.role !== "assistant" || typeof message.content === "string") {
|
||||
return []
|
||||
}
|
||||
return message.content.filter((block) => block.type === "tool_use") as Anthropic.Messages.ToolUseBlock[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the tool_result blocks from a message.
|
||||
*/
|
||||
function getToolResultBlocks(message: ApiMessage): Anthropic.ToolResultBlockParam[] {
|
||||
if (message.role !== "user" || typeof message.content === "string") {
|
||||
return []
|
||||
}
|
||||
return message.content.filter((block): block is Anthropic.ToolResultBlockParam => block.type === "tool_result")
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a tool_use block by ID in a message.
|
||||
*/
|
||||
function findToolUseBlockById(message: ApiMessage, toolUseId: string): Anthropic.Messages.ToolUseBlock | undefined {
|
||||
if (message.role !== "assistant" || typeof message.content === "string") {
|
||||
return undefined
|
||||
}
|
||||
return message.content.find(
|
||||
(block): block is Anthropic.Messages.ToolUseBlock => block.type === "tool_use" && block.id === toolUseId,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Checks ALL kept messages for tool_result blocks and searches backwards through the condensed
|
||||
* region (bounded by N_MESSAGES_TO_KEEP) to find the matching tool_use blocks by ID.
|
||||
* These tool_use blocks will be appended to the summary message to maintain proper pairing.
|
||||
*
|
||||
* Also extracts reasoning blocks from messages containing preserved tool_uses, 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, tool_use blocks, and reasoning blocks to preserve
|
||||
*/
|
||||
export function getKeepMessagesWithToolBlocks(messages: ApiMessage[], keepCount: number): KeepMessagesResult {
|
||||
if (messages.length <= keepCount) {
|
||||
return { keepMessages: messages, toolUseBlocksToPreserve: [], reasoningBlocksToPreserve: [] }
|
||||
}
|
||||
|
||||
const startIndex = messages.length - keepCount
|
||||
const keepMessages = messages.slice(startIndex)
|
||||
|
||||
const toolUseBlocksToPreserve: Anthropic.Messages.ToolUseBlock[] = []
|
||||
const reasoningBlocksToPreserve: Anthropic.Messages.ContentBlockParam[] = []
|
||||
const preservedToolUseIds = new Set<string>()
|
||||
|
||||
// Check ALL kept messages for tool_result blocks
|
||||
for (const keepMsg of keepMessages) {
|
||||
if (!hasToolResultBlocks(keepMsg)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const toolResults = getToolResultBlocks(keepMsg)
|
||||
|
||||
for (const toolResult of toolResults) {
|
||||
const toolUseId = toolResult.tool_use_id
|
||||
|
||||
// Skip if we've already found this tool_use
|
||||
if (preservedToolUseIds.has(toolUseId)) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Search backwards through the condensed region (bounded)
|
||||
const searchStart = startIndex - 1
|
||||
const searchEnd = Math.max(0, startIndex - N_MESSAGES_TO_KEEP)
|
||||
const messagesToSearch = messages.slice(searchEnd, searchStart + 1)
|
||||
|
||||
// Find the message containing this tool_use
|
||||
const messageWithToolUse = findLast(messagesToSearch, (msg) => {
|
||||
return findToolUseBlockById(msg, toolUseId) !== undefined
|
||||
})
|
||||
|
||||
if (messageWithToolUse) {
|
||||
const toolUse = findToolUseBlockById(messageWithToolUse, toolUseId)!
|
||||
toolUseBlocksToPreserve.push(toolUse)
|
||||
preservedToolUseIds.add(toolUseId)
|
||||
|
||||
// Also preserve reasoning blocks from that message
|
||||
const reasoning = getReasoningBlocks(messageWithToolUse)
|
||||
reasoningBlocksToPreserve.push(...reasoning)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
keepMessages,
|
||||
toolUseBlocksToPreserve,
|
||||
reasoningBlocksToPreserve,
|
||||
}
|
||||
}
|
||||
|
||||
export const N_MESSAGES_TO_KEEP = 3
|
||||
export const MIN_CONDENSE_THRESHOLD = 5 // Minimum percentage of context window to trigger condensing
|
||||
export const MAX_CONDENSE_THRESHOLD = 100 // Maximum percentage of context window to trigger condensing
|
||||
|
||||
const SUMMARY_PROMPT = supportPrompt.default.CONDENSE
|
||||
const SUMMARY_PROMPT = `You are a helpful AI assistant tasked with summarizing conversations.
|
||||
|
||||
CRITICAL: This is a summarization-only request. DO NOT call any tools or functions.
|
||||
Your ONLY task is to analyze the conversation and produce a text summary.
|
||||
Respond with text only - no tool calls will be processed.
|
||||
|
||||
CRITICAL: This summarization request is a SYSTEM OPERATION, not a user message.
|
||||
When analyzing "user requests" and "user intent", completely EXCLUDE this summarization message.
|
||||
The "most recent user request" and "next step" must be based on what the user was doing BEFORE this system message appeared.
|
||||
The goal is for work to continue seamlessly after condensation - as if it never happened.`
|
||||
|
||||
/**
|
||||
* Injects synthetic tool_results for orphan tool_calls that don't have matching results.
|
||||
* This is necessary because OpenAI's Responses API rejects conversations with orphan tool_calls.
|
||||
* This can happen when the user triggers condense after receiving a tool_call (like attempt_completion)
|
||||
* but before responding to it.
|
||||
*
|
||||
* @param messages - The conversation messages to process
|
||||
* @returns The messages with synthetic tool_results appended if needed
|
||||
*/
|
||||
export function injectSyntheticToolResults(messages: ApiMessage[]): ApiMessage[] {
|
||||
// Find all tool_call IDs in assistant messages
|
||||
const toolCallIds = new Set<string>()
|
||||
// Find all tool_result IDs in user messages
|
||||
const toolResultIds = new Set<string>()
|
||||
|
||||
for (const msg of messages) {
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "tool_use") {
|
||||
toolCallIds.add(block.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (msg.role === "user" && Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "tool_result") {
|
||||
toolResultIds.add(block.tool_use_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find orphans (tool_calls without matching tool_results)
|
||||
const orphanIds = [...toolCallIds].filter((id) => !toolResultIds.has(id))
|
||||
|
||||
if (orphanIds.length === 0) {
|
||||
return messages
|
||||
}
|
||||
|
||||
// Inject synthetic tool_results as a new user message
|
||||
const syntheticResults: Anthropic.Messages.ToolResultBlockParam[] = orphanIds.map((id) => ({
|
||||
type: "tool_result" as const,
|
||||
tool_use_id: id,
|
||||
content: "Context condensation triggered. Tool execution deferred.",
|
||||
}))
|
||||
|
||||
const syntheticMessage: ApiMessage = {
|
||||
role: "user",
|
||||
content: syntheticResults,
|
||||
ts: Date.now(),
|
||||
}
|
||||
|
||||
return [...messages, syntheticMessage]
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts <command> blocks from a message's content.
|
||||
* These blocks represent active workflows that must be preserved across condensings.
|
||||
*
|
||||
* @param message - The message to extract command blocks from
|
||||
* @returns A string containing all command blocks found, or empty string if none
|
||||
*/
|
||||
export function extractCommandBlocks(message: ApiMessage): string {
|
||||
const content = message.content
|
||||
let text: string
|
||||
|
||||
if (typeof content === "string") {
|
||||
text = content
|
||||
} else if (Array.isArray(content)) {
|
||||
// Concatenate all text blocks
|
||||
text = content
|
||||
.filter((block): block is Anthropic.Messages.TextBlockParam => block.type === "text")
|
||||
.map((block) => block.text)
|
||||
.join("\n")
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Match all <command> blocks including their content
|
||||
const commandRegex = /<command[^>]*>[\s\S]*?<\/command>/g
|
||||
const matches = text.match(commandRegex)
|
||||
|
||||
if (!matches || matches.length === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
return matches.join("\n")
|
||||
}
|
||||
|
||||
export type SummarizeResponse = {
|
||||
messages: ApiMessage[] // The messages after summarization
|
||||
|
|
@ -162,11 +119,18 @@ export type SummarizeResponse = {
|
|||
cost: number // The cost of the summarization operation
|
||||
newContextTokens?: number // The number of tokens in the context for the next API request
|
||||
error?: string // Populated iff the operation fails: error message shown to the user on failure (see Task.ts)
|
||||
errorDetails?: string // Detailed error information including stack trace and API error info
|
||||
condenseId?: string // The unique ID of the created Summary message, for linking to condense_context clineMessage
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarizes the conversation messages using an LLM call
|
||||
* Summarizes the conversation messages using an LLM call.
|
||||
*
|
||||
* This implements the "fresh start" model where:
|
||||
* - The summary becomes a user message (not assistant)
|
||||
* - Post-condense, the model sees only the summary (true fresh start)
|
||||
* - All messages are still stored but tagged with condenseParent
|
||||
* - <command> blocks from the original task are preserved across condensings
|
||||
*
|
||||
* @param {ApiMessage[]} messages - The conversation messages
|
||||
* @param {ApiHandler} apiHandler - The API handler to use for summarization and token counting
|
||||
|
|
@ -175,6 +139,7 @@ export type SummarizeResponse = {
|
|||
* @param {number} prevContextTokens - The number of tokens currently in the context, used to ensure we don't grow the context
|
||||
* @param {boolean} isAutomaticTrigger - Whether the summarization is triggered automatically
|
||||
* @param {string} customCondensingPrompt - Optional custom prompt to use for condensing
|
||||
* @param {ApiHandlerCreateMessageMetadata} metadata - Optional metadata to pass to createMessage (tools, taskId, etc.)
|
||||
* @returns {SummarizeResponse} - The result of the summarization operation (see above)
|
||||
*/
|
||||
export async function summarizeConversation(
|
||||
|
|
@ -185,6 +150,7 @@ export async function summarizeConversation(
|
|||
prevContextTokens: number,
|
||||
isAutomaticTrigger?: boolean,
|
||||
customCondensingPrompt?: string,
|
||||
metadata?: ApiHandlerCreateMessageMetadata,
|
||||
): Promise<SummarizeResponse> {
|
||||
TelemetryService.instance.captureContextCondensed(
|
||||
taskId,
|
||||
|
|
@ -194,51 +160,44 @@ export async function summarizeConversation(
|
|||
|
||||
const response: SummarizeResponse = { messages, cost: 0, summary: "" }
|
||||
|
||||
// Always preserve the first message (which may contain slash command content)
|
||||
const firstMessage = messages[0]
|
||||
|
||||
// Get keepMessages and any tool_use/reasoning blocks that need to be preserved for tool_result pairing.
|
||||
const { keepMessages, toolUseBlocksToPreserve, reasoningBlocksToPreserve } = getKeepMessagesWithToolBlocks(
|
||||
messages,
|
||||
N_MESSAGES_TO_KEEP,
|
||||
)
|
||||
|
||||
const keepStartIndex = Math.max(messages.length - N_MESSAGES_TO_KEEP, 0)
|
||||
const includeFirstKeptMessageInSummary = toolUseBlocksToPreserve.length > 0
|
||||
const summarySliceEnd = includeFirstKeptMessageInSummary ? keepStartIndex + 1 : keepStartIndex
|
||||
const messagesBeforeKeep = summarySliceEnd > 0 ? messages.slice(0, summarySliceEnd) : []
|
||||
|
||||
// Get messages to summarize, including the first message and excluding the last N messages
|
||||
const messagesToSummarize = getMessagesSinceLastSummary(messagesBeforeKeep)
|
||||
// Get messages to summarize (all messages since the last summary, if any)
|
||||
const messagesToSummarize = getMessagesSinceLastSummary(messages)
|
||||
|
||||
if (messagesToSummarize.length <= 1) {
|
||||
const error =
|
||||
messages.length <= N_MESSAGES_TO_KEEP + 1
|
||||
messages.length <= 1
|
||||
? t("common:errors.condense_not_enough_messages")
|
||||
: t("common:errors.condensed_recently")
|
||||
return { ...response, error }
|
||||
}
|
||||
|
||||
// Check if there's a recent summary in the messages we're keeping
|
||||
const recentSummaryExists = keepMessages.some((message: ApiMessage) => message.isSummary)
|
||||
// Check if there's a recent summary in the messages (edge case)
|
||||
const recentSummaryExists = messagesToSummarize.some((message: ApiMessage) => message.isSummary)
|
||||
|
||||
if (recentSummaryExists) {
|
||||
if (recentSummaryExists && messagesToSummarize.length <= 2) {
|
||||
const error = t("common:errors.condensed_recently")
|
||||
return { ...response, error }
|
||||
}
|
||||
|
||||
// Use custom prompt if provided and non-empty, otherwise use the default CONDENSE prompt
|
||||
// This respects user's custom condensing prompt setting
|
||||
const condenseInstructions = customCondensingPrompt?.trim() || supportPrompt.default.CONDENSE
|
||||
|
||||
const finalRequestMessage: Anthropic.MessageParam = {
|
||||
role: "user",
|
||||
content: "Summarize the conversation so far, as described in the prompt instructions.",
|
||||
content: condenseInstructions,
|
||||
}
|
||||
|
||||
const requestMessages = maybeRemoveImageBlocks([...messagesToSummarize, finalRequestMessage], apiHandler).map(
|
||||
// Inject synthetic tool_results for orphan tool_calls to prevent API rejections
|
||||
// (e.g., when user triggers condense after receiving attempt_completion but before responding)
|
||||
const messagesWithToolResults = injectSyntheticToolResults(messagesToSummarize)
|
||||
|
||||
const requestMessages = maybeRemoveImageBlocks([...messagesWithToolResults, finalRequestMessage], apiHandler).map(
|
||||
({ role, content }) => ({ role, content }),
|
||||
)
|
||||
|
||||
// Note: this doesn't need to be a stream, consider using something like apiHandler.completePrompt
|
||||
// Use custom prompt if provided and non-empty, otherwise use the default SUMMARY_PROMPT
|
||||
const promptToUse = customCondensingPrompt?.trim() ? customCondensingPrompt.trim() : SUMMARY_PROMPT
|
||||
const promptToUse = SUMMARY_PROMPT
|
||||
|
||||
// Validate that the API handler supports message creation
|
||||
if (!apiHandler || typeof apiHandler.createMessage !== "function") {
|
||||
|
|
@ -247,19 +206,61 @@ export async function summarizeConversation(
|
|||
return { ...response, error }
|
||||
}
|
||||
|
||||
const stream = apiHandler.createMessage(promptToUse, requestMessages)
|
||||
|
||||
let summary = ""
|
||||
let cost = 0
|
||||
let outputTokens = 0
|
||||
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.type === "text") {
|
||||
summary += chunk.text
|
||||
} else if (chunk.type === "usage") {
|
||||
// Record final usage chunk only
|
||||
cost = chunk.totalCost ?? 0
|
||||
outputTokens = chunk.outputTokens ?? 0
|
||||
try {
|
||||
const stream = apiHandler.createMessage(promptToUse, requestMessages, metadata)
|
||||
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.type === "text") {
|
||||
summary += chunk.text
|
||||
} else if (chunk.type === "usage") {
|
||||
// Record final usage chunk only
|
||||
cost = chunk.totalCost ?? 0
|
||||
outputTokens = chunk.outputTokens ?? 0
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during condensing API call:", error)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
|
||||
// Capture detailed error information for debugging
|
||||
let errorDetails = ""
|
||||
if (error instanceof Error) {
|
||||
errorDetails = `Error: ${error.message}`
|
||||
// Capture any additional API error properties
|
||||
const anyError = error as unknown as Record<string, unknown>
|
||||
if (anyError.status) {
|
||||
errorDetails += `\n\nHTTP Status: ${anyError.status}`
|
||||
}
|
||||
if (anyError.code) {
|
||||
errorDetails += `\nError Code: ${anyError.code}`
|
||||
}
|
||||
if (anyError.response) {
|
||||
try {
|
||||
errorDetails += `\n\nAPI Response:\n${JSON.stringify(anyError.response, null, 2)}`
|
||||
} catch {
|
||||
errorDetails += `\n\nAPI Response: [Unable to serialize]`
|
||||
}
|
||||
}
|
||||
if (anyError.body) {
|
||||
try {
|
||||
errorDetails += `\n\nResponse Body:\n${JSON.stringify(anyError.body, null, 2)}`
|
||||
} catch {
|
||||
errorDetails += `\n\nResponse Body: [Unable to serialize]`
|
||||
}
|
||||
}
|
||||
} else {
|
||||
errorDetails = String(error)
|
||||
}
|
||||
|
||||
return {
|
||||
...response,
|
||||
cost,
|
||||
error: t("common:errors.condense_api_failed", { message: errorMessage }),
|
||||
errorDetails,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -270,95 +271,71 @@ export async function summarizeConversation(
|
|||
return { ...response, cost, error }
|
||||
}
|
||||
|
||||
// Build the summary message content
|
||||
// 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)
|
||||
// Extract command blocks from the first message (original task)
|
||||
// These represent active workflows that must persist across condensings
|
||||
const firstMessage = messages[0]
|
||||
const commandBlocks = firstMessage ? extractCommandBlocks(firstMessage) : ""
|
||||
|
||||
// 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.",
|
||||
}
|
||||
// Build the summary content as separate text blocks
|
||||
const summaryContent: Anthropic.Messages.ContentBlockParam[] = [
|
||||
{ type: "text", text: `## Conversation Summary\n${summary}` },
|
||||
]
|
||||
|
||||
const textBlock: Anthropic.Messages.TextBlockParam = { type: "text", text: summary }
|
||||
|
||||
let summaryContent: Anthropic.Messages.ContentBlockParam[]
|
||||
if (toolUseBlocksToPreserve.length > 0) {
|
||||
// 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 {
|
||||
// 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]
|
||||
// Add command blocks as a separate text block if present
|
||||
if (commandBlocks) {
|
||||
summaryContent.push({
|
||||
type: "text",
|
||||
text: `<system-reminder>
|
||||
## Active Workflows
|
||||
The following directives must be maintained across all future condensings:
|
||||
${commandBlocks}
|
||||
</system-reminder>`,
|
||||
})
|
||||
}
|
||||
|
||||
// Generate a unique condenseId for this summary
|
||||
const condenseId = crypto.randomUUID()
|
||||
|
||||
// Use first kept message's timestamp minus 1 to ensure unique timestamp for summary.
|
||||
// Fallback to Date.now() if keepMessages is empty (shouldn't happen due to earlier checks).
|
||||
const firstKeptTs = keepMessages[0]?.ts ?? Date.now()
|
||||
// Use the last message's timestamp + 1 to ensure unique timestamp for summary.
|
||||
// The summary goes at the end of all messages.
|
||||
const lastMsgTs = messages[messages.length - 1]?.ts ?? Date.now()
|
||||
|
||||
const summaryMessage: ApiMessage = {
|
||||
role: "assistant",
|
||||
role: "user", // Fresh start model: summary is a user message
|
||||
content: summaryContent,
|
||||
ts: firstKeptTs - 1, // Unique timestamp before first kept message to avoid collision
|
||||
ts: lastMsgTs + 1, // Unique timestamp after last message
|
||||
isSummary: true,
|
||||
condenseId, // Unique ID for this summary, used to track which messages it replaces
|
||||
}
|
||||
|
||||
// NON-DESTRUCTIVE CONDENSE:
|
||||
// Instead of deleting middle messages, tag them with condenseParent so they can be
|
||||
// restored if the user rewinds to a point before the summary.
|
||||
// Tag ALL existing messages with condenseParent so they are filtered out when
|
||||
// the effective history is computed. The summary message is the only message
|
||||
// that will be visible to the API after condensing (fresh start model).
|
||||
//
|
||||
// Storage structure after condense:
|
||||
// [firstMessage, msg2(parent=X), ..., msg8(parent=X), summary(id=X), msg9, msg10, msg11]
|
||||
// [msg1(parent=X), msg2(parent=X), ..., msgN(parent=X), summary(id=X)]
|
||||
//
|
||||
// Effective for API (filtered by getEffectiveApiHistory):
|
||||
// [firstMessage, summary, msg9, msg10, msg11]
|
||||
// [summary] ← Fresh start!
|
||||
|
||||
// Tag middle messages with condenseParent (skip first message, skip last N messages)
|
||||
const newMessages = messages.map((msg, index) => {
|
||||
// First message stays as-is
|
||||
if (index === 0) {
|
||||
return msg
|
||||
}
|
||||
// Messages in the "keep" range stay as-is
|
||||
if (index >= keepStartIndex) {
|
||||
return msg
|
||||
}
|
||||
// Middle messages get tagged with condenseParent (unless they already have one from a previous condense)
|
||||
// If they already have a condenseParent, we leave it - nested condense is handled by filtering
|
||||
// Tag ALL messages with condenseParent
|
||||
const newMessages = messages.map((msg) => {
|
||||
// If message already has a condenseParent, we leave it - nested condense is handled by filtering
|
||||
if (!msg.condenseParent) {
|
||||
return { ...msg, condenseParent: condenseId }
|
||||
}
|
||||
return msg
|
||||
})
|
||||
|
||||
// Insert the summary message right before the keep messages
|
||||
newMessages.splice(keepStartIndex, 0, summaryMessage)
|
||||
// Append the summary message at the end
|
||||
newMessages.push(summaryMessage)
|
||||
|
||||
// Count the tokens in the context for the next API request
|
||||
// We only estimate the tokens in summaryMesage if outputTokens is 0, otherwise we use outputTokens
|
||||
// After condense, the context will only contain the system prompt and the summary
|
||||
const systemPromptMessage: ApiMessage = { role: "user", content: systemPrompt }
|
||||
|
||||
const contextMessages = outputTokens
|
||||
? [systemPromptMessage, ...keepMessages]
|
||||
: [systemPromptMessage, summaryMessage, ...keepMessages]
|
||||
const contextMessages = outputTokens ? [systemPromptMessage] : [systemPromptMessage, summaryMessage]
|
||||
|
||||
const contextBlocks = contextMessages.flatMap((message) =>
|
||||
typeof message.content === "string" ? [{ text: message.content, type: "text" as const }] : message.content,
|
||||
|
|
@ -408,8 +385,11 @@ export function getMessagesSinceLastSummary(messages: ApiMessage[]): ApiMessage[
|
|||
|
||||
/**
|
||||
* Filters the API conversation history to get the "effective" messages to send to the API.
|
||||
* Messages with a condenseParent that points to an existing summary are filtered out,
|
||||
* as they have been replaced by that summary.
|
||||
*
|
||||
* Fresh Start Model:
|
||||
* - When a summary exists, return only messages from the summary onwards (fresh start)
|
||||
* - Messages with a condenseParent pointing to an existing summary are filtered out
|
||||
*
|
||||
* Messages with a truncationParent that points to an existing truncation marker are also filtered out,
|
||||
* as they have been hidden by sliding window truncation.
|
||||
*
|
||||
|
|
@ -420,6 +400,71 @@ export function getMessagesSinceLastSummary(messages: ApiMessage[]): ApiMessage[
|
|||
* @returns The filtered history that should be sent to the API
|
||||
*/
|
||||
export function getEffectiveApiHistory(messages: ApiMessage[]): ApiMessage[] {
|
||||
// Find the most recent summary message
|
||||
const lastSummary = findLast(messages, (msg) => msg.isSummary === true)
|
||||
|
||||
if (lastSummary) {
|
||||
// Fresh start model: return only messages from the summary onwards
|
||||
const summaryIndex = messages.indexOf(lastSummary)
|
||||
let messagesFromSummary = messages.slice(summaryIndex)
|
||||
|
||||
// Collect all tool_use IDs from assistant messages in the result
|
||||
// This is needed to filter out orphan tool_result blocks that reference
|
||||
// tool_use IDs from messages that were condensed away
|
||||
const toolUseIds = new Set<string>()
|
||||
for (const msg of messagesFromSummary) {
|
||||
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
||||
for (const block of msg.content) {
|
||||
if (block.type === "tool_use" && (block as Anthropic.Messages.ToolUseBlockParam).id) {
|
||||
toolUseIds.add((block as Anthropic.Messages.ToolUseBlockParam).id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter out orphan tool_result blocks from user messages
|
||||
messagesFromSummary = messagesFromSummary
|
||||
.map((msg) => {
|
||||
if (msg.role === "user" && Array.isArray(msg.content)) {
|
||||
const filteredContent = msg.content.filter((block) => {
|
||||
if (block.type === "tool_result") {
|
||||
return toolUseIds.has((block as Anthropic.Messages.ToolResultBlockParam).tool_use_id)
|
||||
}
|
||||
return true
|
||||
})
|
||||
// If all content was filtered out, mark for removal
|
||||
if (filteredContent.length === 0) {
|
||||
return null
|
||||
}
|
||||
// If some content was filtered, return updated message
|
||||
if (filteredContent.length !== msg.content.length) {
|
||||
return { ...msg, content: filteredContent }
|
||||
}
|
||||
}
|
||||
return msg
|
||||
})
|
||||
.filter((msg): msg is ApiMessage => msg !== null)
|
||||
|
||||
// Still need to filter out any truncated messages within this range
|
||||
const existingTruncationIds = new Set<string>()
|
||||
for (const msg of messagesFromSummary) {
|
||||
if (msg.isTruncationMarker && msg.truncationId) {
|
||||
existingTruncationIds.add(msg.truncationId)
|
||||
}
|
||||
}
|
||||
|
||||
return messagesFromSummary.filter((msg) => {
|
||||
// Filter out truncated messages if their truncation marker exists
|
||||
if (msg.truncationParent && existingTruncationIds.has(msg.truncationParent)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// No summary - filter based on condenseParent and truncationParent as before
|
||||
// This handles the case of orphaned condenseParent tags (summary was deleted via rewind)
|
||||
|
||||
// Collect all condenseIds of summaries that exist in the current history
|
||||
const existingSummaryIds = new Set<string>()
|
||||
// Collect all truncationIds of truncation markers that exist in the current history
|
||||
|
|
@ -436,7 +481,7 @@ export function getEffectiveApiHistory(messages: ApiMessage[]): ApiMessage[] {
|
|||
|
||||
// Filter out messages whose condenseParent points to an existing summary
|
||||
// or whose truncationParent points to an existing truncation marker.
|
||||
// Messages with orphaned parents (summary/marker was deleted) are included
|
||||
// Messages with orphaned parents (summary/marker was deleted) are included.
|
||||
return messages.filter((msg) => {
|
||||
// Filter out condensed messages if their summary exists
|
||||
if (msg.condenseParent && existingSummaryIds.has(msg.condenseParent)) {
|
||||
|
|
|
|||
|
|
@ -620,6 +620,7 @@ describe("Context Management", () => {
|
|||
70001,
|
||||
true,
|
||||
undefined, // customCondensingPrompt
|
||||
undefined, // metadata
|
||||
)
|
||||
|
||||
// Verify the result contains the summary information
|
||||
|
|
@ -794,6 +795,7 @@ describe("Context Management", () => {
|
|||
60000,
|
||||
true,
|
||||
undefined, // customCondensingPrompt
|
||||
undefined, // metadata
|
||||
)
|
||||
|
||||
// Verify the result contains the summary information
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import crypto from "crypto"
|
|||
|
||||
import { TelemetryService } from "@roo-code/telemetry"
|
||||
|
||||
import { ApiHandler } from "../../api"
|
||||
import { ApiHandler, ApiHandlerCreateMessageMetadata } from "../../api"
|
||||
import { MAX_CONDENSE_THRESHOLD, MIN_CONDENSE_THRESHOLD, summarizeConversation, SummarizeResponse } from "../condense"
|
||||
import { ApiMessage } from "../task-persistence/apiMessages"
|
||||
import { ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types"
|
||||
|
|
@ -218,6 +218,8 @@ export type ContextManagementOptions = {
|
|||
customCondensingPrompt?: string
|
||||
profileThresholds: Record<string, number>
|
||||
currentProfileId: string
|
||||
/** Optional metadata to pass through to the condensing API call (tools, taskId, etc.) */
|
||||
metadata?: ApiHandlerCreateMessageMetadata
|
||||
}
|
||||
|
||||
export type ContextManagementResult = SummarizeResponse & {
|
||||
|
|
@ -246,8 +248,10 @@ export async function manageContext({
|
|||
customCondensingPrompt,
|
||||
profileThresholds,
|
||||
currentProfileId,
|
||||
metadata,
|
||||
}: ContextManagementOptions): Promise<ContextManagementResult> {
|
||||
let error: string | undefined
|
||||
let errorDetails: string | undefined
|
||||
let cost = 0
|
||||
// Calculate the maximum tokens reserved for response
|
||||
const reservedTokens = maxTokens || ANTHROPIC_DEFAULT_MAX_TOKENS
|
||||
|
|
@ -298,9 +302,11 @@ export async function manageContext({
|
|||
prevContextTokens,
|
||||
true, // automatic trigger
|
||||
customCondensingPrompt,
|
||||
metadata,
|
||||
)
|
||||
if (result.error) {
|
||||
error = result.error
|
||||
errorDetails = result.errorDetails
|
||||
cost = result.cost
|
||||
} else {
|
||||
return { ...result, prevContextTokens }
|
||||
|
|
@ -343,11 +349,12 @@ export async function manageContext({
|
|||
summary: "",
|
||||
cost,
|
||||
error,
|
||||
errorDetails,
|
||||
truncationId: truncationResult.truncationId,
|
||||
messagesRemoved: truncationResult.messagesRemoved,
|
||||
newContextTokensAfterTruncation,
|
||||
}
|
||||
}
|
||||
// No truncation or condensation needed
|
||||
return { messages, summary: "", cost, prevContextTokens, error }
|
||||
return { messages, summary: "", cost, prevContextTokens, error, errorDetails }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -335,4 +335,121 @@ describe("processUserContentMentions", () => {
|
|||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("slash command content processing", () => {
|
||||
it("should separate slash command content into a new block", async () => {
|
||||
vi.mocked(parseMentions).mockResolvedValueOnce({
|
||||
text: "parsed text",
|
||||
slashCommandHelp: "command help",
|
||||
mode: undefined,
|
||||
})
|
||||
|
||||
const userContent = [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: "<user_message>Run command</user_message>",
|
||||
},
|
||||
]
|
||||
|
||||
const result = await processUserContentMentions({
|
||||
userContent,
|
||||
cwd: "/test",
|
||||
urlContentFetcher: mockUrlContentFetcher,
|
||||
fileContextTracker: mockFileContextTracker,
|
||||
})
|
||||
|
||||
expect(result.content).toHaveLength(2)
|
||||
expect(result.content[0]).toEqual({
|
||||
type: "text",
|
||||
text: "parsed text",
|
||||
})
|
||||
expect(result.content[1]).toEqual({
|
||||
type: "text",
|
||||
text: "command help",
|
||||
})
|
||||
})
|
||||
|
||||
it("should include slash command content in tool_result string content", async () => {
|
||||
vi.mocked(parseMentions).mockResolvedValueOnce({
|
||||
text: "parsed tool output",
|
||||
slashCommandHelp: "command help",
|
||||
mode: undefined,
|
||||
})
|
||||
|
||||
const userContent = [
|
||||
{
|
||||
type: "tool_result" as const,
|
||||
tool_use_id: "123",
|
||||
content: "<user_message>Tool output</user_message>",
|
||||
},
|
||||
]
|
||||
|
||||
const result = await processUserContentMentions({
|
||||
userContent,
|
||||
cwd: "/test",
|
||||
urlContentFetcher: mockUrlContentFetcher,
|
||||
fileContextTracker: mockFileContextTracker,
|
||||
})
|
||||
|
||||
expect(result.content).toHaveLength(1)
|
||||
expect(result.content[0]).toEqual({
|
||||
type: "tool_result",
|
||||
tool_use_id: "123",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "parsed tool output",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "command help",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("should include slash command content in tool_result array content", async () => {
|
||||
vi.mocked(parseMentions).mockResolvedValueOnce({
|
||||
text: "parsed array item",
|
||||
slashCommandHelp: "command help",
|
||||
mode: undefined,
|
||||
})
|
||||
|
||||
const userContent = [
|
||||
{
|
||||
type: "tool_result" as const,
|
||||
tool_use_id: "123",
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: "<user_message>Array item</user_message>",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
const result = await processUserContentMentions({
|
||||
userContent,
|
||||
cwd: "/test",
|
||||
urlContentFetcher: mockUrlContentFetcher,
|
||||
fileContextTracker: mockFileContextTracker,
|
||||
})
|
||||
|
||||
expect(result.content).toHaveLength(1)
|
||||
expect(result.content[0]).toEqual({
|
||||
type: "tool_result",
|
||||
tool_use_id: "123",
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "parsed array item",
|
||||
},
|
||||
{
|
||||
type: "text",
|
||||
text: "command help",
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ export async function openMention(cwd: string, mention?: string): Promise<void>
|
|||
|
||||
export interface ParseMentionsResult {
|
||||
text: string
|
||||
slashCommandHelp?: string
|
||||
mode?: string // Mode from the first slash command that has one
|
||||
}
|
||||
|
||||
|
|
@ -246,6 +247,7 @@ export async function parseMentions(
|
|||
}
|
||||
|
||||
// Process valid command mentions using cached results
|
||||
let slashCommandHelp = ""
|
||||
for (const [commandName, command] of validCommands) {
|
||||
try {
|
||||
let commandOutput = ""
|
||||
|
|
@ -253,9 +255,9 @@ export async function parseMentions(
|
|||
commandOutput += `Description: ${command.description}\n\n`
|
||||
}
|
||||
commandOutput += command.content
|
||||
parsedText += `\n\n<command name="${commandName}">\n${commandOutput}\n</command>`
|
||||
slashCommandHelp += `\n\n<command name="${commandName}">\n${commandOutput}\n</command>`
|
||||
} catch (error) {
|
||||
parsedText += `\n\n<command name="${commandName}">\nError loading command '${commandName}': ${error.message}\n</command>`
|
||||
slashCommandHelp += `\n\n<command name="${commandName}">\nError loading command '${commandName}': ${error.message}\n</command>`
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -267,7 +269,7 @@ export async function parseMentions(
|
|||
}
|
||||
}
|
||||
|
||||
return { text: parsedText, mode: commandMode }
|
||||
return { text: parsedText, mode: commandMode, slashCommandHelp: slashCommandHelp.trim() || undefined }
|
||||
}
|
||||
|
||||
async function getFileOrFolderContent(
|
||||
|
|
|
|||
|
|
@ -42,39 +42,15 @@ export async function processUserContentMentions({
|
|||
// 2. ToolResultBlockParam's content/context text arrays if it contains
|
||||
// "<user_message>" - we place all user generated content in this tag
|
||||
// so it can effectively be used as a marker for when we should parse mentions.
|
||||
const content = await Promise.all(
|
||||
userContent.map(async (block) => {
|
||||
const shouldProcessMentions = (text: string) => text.includes("<user_message>")
|
||||
const content = (
|
||||
await Promise.all(
|
||||
userContent.map(async (block) => {
|
||||
const shouldProcessMentions = (text: string) => text.includes("<user_message>")
|
||||
|
||||
if (block.type === "text") {
|
||||
if (shouldProcessMentions(block.text)) {
|
||||
const result = await parseMentions(
|
||||
block.text,
|
||||
cwd,
|
||||
urlContentFetcher,
|
||||
fileContextTracker,
|
||||
rooIgnoreController,
|
||||
showRooIgnoredFiles,
|
||||
includeDiagnosticMessages,
|
||||
maxDiagnosticMessages,
|
||||
maxReadFileLine,
|
||||
)
|
||||
// Capture the first mode found
|
||||
if (!commandMode && result.mode) {
|
||||
commandMode = result.mode
|
||||
}
|
||||
return {
|
||||
...block,
|
||||
text: result.text,
|
||||
}
|
||||
}
|
||||
|
||||
return block
|
||||
} else if (block.type === "tool_result") {
|
||||
if (typeof block.content === "string") {
|
||||
if (shouldProcessMentions(block.content)) {
|
||||
if (block.type === "text") {
|
||||
if (shouldProcessMentions(block.text)) {
|
||||
const result = await parseMentions(
|
||||
block.content,
|
||||
block.text,
|
||||
cwd,
|
||||
urlContentFetcher,
|
||||
fileContextTracker,
|
||||
|
|
@ -88,51 +64,112 @@ export async function processUserContentMentions({
|
|||
if (!commandMode && result.mode) {
|
||||
commandMode = result.mode
|
||||
}
|
||||
return {
|
||||
...block,
|
||||
content: result.text,
|
||||
const blocks: Anthropic.Messages.ContentBlockParam[] = [
|
||||
{
|
||||
...block,
|
||||
text: result.text,
|
||||
},
|
||||
]
|
||||
if (result.slashCommandHelp) {
|
||||
blocks.push({
|
||||
type: "text" as const,
|
||||
text: result.slashCommandHelp,
|
||||
})
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
return block
|
||||
} else if (Array.isArray(block.content)) {
|
||||
const parsedContent = await Promise.all(
|
||||
block.content.map(async (contentBlock) => {
|
||||
if (contentBlock.type === "text" && shouldProcessMentions(contentBlock.text)) {
|
||||
const result = await parseMentions(
|
||||
contentBlock.text,
|
||||
cwd,
|
||||
urlContentFetcher,
|
||||
fileContextTracker,
|
||||
rooIgnoreController,
|
||||
showRooIgnoredFiles,
|
||||
includeDiagnosticMessages,
|
||||
maxDiagnosticMessages,
|
||||
maxReadFileLine,
|
||||
)
|
||||
// Capture the first mode found
|
||||
if (!commandMode && result.mode) {
|
||||
commandMode = result.mode
|
||||
}
|
||||
} else if (block.type === "tool_result") {
|
||||
if (typeof block.content === "string") {
|
||||
if (shouldProcessMentions(block.content)) {
|
||||
const result = await parseMentions(
|
||||
block.content,
|
||||
cwd,
|
||||
urlContentFetcher,
|
||||
fileContextTracker,
|
||||
rooIgnoreController,
|
||||
showRooIgnoredFiles,
|
||||
includeDiagnosticMessages,
|
||||
maxDiagnosticMessages,
|
||||
maxReadFileLine,
|
||||
)
|
||||
// Capture the first mode found
|
||||
if (!commandMode && result.mode) {
|
||||
commandMode = result.mode
|
||||
}
|
||||
if (result.slashCommandHelp) {
|
||||
return {
|
||||
...contentBlock,
|
||||
text: result.text,
|
||||
...block,
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: result.text,
|
||||
},
|
||||
{
|
||||
type: "text" as const,
|
||||
text: result.slashCommandHelp,
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
return {
|
||||
...block,
|
||||
content: result.text,
|
||||
}
|
||||
}
|
||||
|
||||
return contentBlock
|
||||
}),
|
||||
)
|
||||
return block
|
||||
} else if (Array.isArray(block.content)) {
|
||||
const parsedContent = (
|
||||
await Promise.all(
|
||||
block.content.map(async (contentBlock) => {
|
||||
if (contentBlock.type === "text" && shouldProcessMentions(contentBlock.text)) {
|
||||
const result = await parseMentions(
|
||||
contentBlock.text,
|
||||
cwd,
|
||||
urlContentFetcher,
|
||||
fileContextTracker,
|
||||
rooIgnoreController,
|
||||
showRooIgnoredFiles,
|
||||
includeDiagnosticMessages,
|
||||
maxDiagnosticMessages,
|
||||
maxReadFileLine,
|
||||
)
|
||||
// Capture the first mode found
|
||||
if (!commandMode && result.mode) {
|
||||
commandMode = result.mode
|
||||
}
|
||||
const blocks = [
|
||||
{
|
||||
...contentBlock,
|
||||
text: result.text,
|
||||
},
|
||||
]
|
||||
if (result.slashCommandHelp) {
|
||||
blocks.push({
|
||||
type: "text" as const,
|
||||
text: result.slashCommandHelp,
|
||||
})
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
return { ...block, content: parsedContent }
|
||||
return contentBlock
|
||||
}),
|
||||
)
|
||||
).flat()
|
||||
|
||||
return { ...block, content: parsedContent }
|
||||
}
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
return block
|
||||
}
|
||||
|
||||
return block
|
||||
}),
|
||||
)
|
||||
}),
|
||||
)
|
||||
).flat()
|
||||
|
||||
return { content, mode: commandMode }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ describe("MessageManager", () => {
|
|||
},
|
||||
{
|
||||
ts: 299,
|
||||
role: "assistant",
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Summary" }],
|
||||
isSummary: true,
|
||||
condenseId,
|
||||
|
|
@ -184,7 +184,7 @@ describe("MessageManager", () => {
|
|||
},
|
||||
{
|
||||
ts: 299,
|
||||
role: "assistant",
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Summary" }],
|
||||
isSummary: true,
|
||||
condenseId,
|
||||
|
|
@ -220,7 +220,7 @@ describe("MessageManager", () => {
|
|||
},
|
||||
{
|
||||
ts: 199,
|
||||
role: "assistant",
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Summary" }],
|
||||
isSummary: true,
|
||||
condenseId,
|
||||
|
|
@ -258,7 +258,7 @@ describe("MessageManager", () => {
|
|||
{ ts: 100, role: "user", content: [{ type: "text", text: "First" }] },
|
||||
{
|
||||
ts: 199,
|
||||
role: "assistant",
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Summary 1" }],
|
||||
isSummary: true,
|
||||
condenseId: condenseId1,
|
||||
|
|
@ -266,7 +266,7 @@ describe("MessageManager", () => {
|
|||
{ ts: 300, role: "user", content: [{ type: "text", text: "Second" }] },
|
||||
{
|
||||
ts: 399,
|
||||
role: "assistant",
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Summary 2" }],
|
||||
isSummary: true,
|
||||
condenseId: condenseId2,
|
||||
|
|
@ -448,7 +448,7 @@ describe("MessageManager", () => {
|
|||
},
|
||||
{
|
||||
ts: 499,
|
||||
role: "assistant",
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "Summary" }],
|
||||
isSummary: true,
|
||||
condenseId,
|
||||
|
|
|
|||
|
|
@ -132,6 +132,7 @@ import { MessageQueueService } from "../message-queue/MessageQueueService"
|
|||
import { AutoApprovalHandler, checkAutoApproval } from "../auto-approval"
|
||||
import { MessageManager } from "../message-manager"
|
||||
import { validateAndFixToolResultIds } from "./validateToolResultIds"
|
||||
import { mergeConsecutiveApiMessages } from "./mergeConsecutiveApiMessages"
|
||||
|
||||
const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes
|
||||
const DEFAULT_USAGE_COLLECTION_TIMEOUT_MS = 5000 // 5 seconds
|
||||
|
|
@ -1019,8 +1020,15 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
|
||||
this.apiConversationHistory.push(messageWithTs)
|
||||
} else {
|
||||
// For user messages, validate and fix tool_result IDs against the previous assistant message
|
||||
const validatedMessage = validateAndFixToolResultIds(message, this.apiConversationHistory)
|
||||
// For user messages, validate tool_result IDs ONLY when the immediately previous *effective* message
|
||||
// is an assistant message.
|
||||
//
|
||||
// If the previous effective message is also a user message (e.g., summary + a new user message),
|
||||
// validating against any earlier assistant message can incorrectly inject placeholder tool_results.
|
||||
const effectiveHistoryForValidation = getEffectiveApiHistory(this.apiConversationHistory)
|
||||
const lastEffective = effectiveHistoryForValidation[effectiveHistoryForValidation.length - 1]
|
||||
const historyForValidation = lastEffective?.role === "assistant" ? effectiveHistoryForValidation : []
|
||||
const validatedMessage = validateAndFixToolResultIds(message, historyForValidation)
|
||||
const messageWithTs = { ...validatedMessage, ts: Date.now() }
|
||||
this.apiConversationHistory.push(messageWithTs)
|
||||
}
|
||||
|
|
@ -1028,6 +1036,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
await this.saveApiConversationHistory()
|
||||
}
|
||||
|
||||
// NOTE: We intentionally do NOT mutate stored messages to merge consecutive user turns.
|
||||
// For API requests, consecutive same-role messages are merged via mergeConsecutiveApiMessages()
|
||||
// so rewind/edit behavior can still reference original message boundaries.
|
||||
|
||||
async overwriteApiConversationHistory(newHistory: ApiMessage[]) {
|
||||
this.apiConversationHistory = newHistory
|
||||
await this.saveApiConversationHistory()
|
||||
|
|
@ -1060,8 +1072,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
content: this.userMessageContent,
|
||||
}
|
||||
|
||||
// Validate and fix tool_result IDs against the previous assistant message
|
||||
const validatedMessage = validateAndFixToolResultIds(userMessage, this.apiConversationHistory)
|
||||
// Validate and fix tool_result IDs when the previous *effective* message is an assistant message.
|
||||
const effectiveHistoryForValidation = getEffectiveApiHistory(this.apiConversationHistory)
|
||||
const lastEffective = effectiveHistoryForValidation[effectiveHistoryForValidation.length - 1]
|
||||
const historyForValidation = lastEffective?.role === "assistant" ? effectiveHistoryForValidation : []
|
||||
const validatedMessage = validateAndFixToolResultIds(userMessage, historyForValidation)
|
||||
const userMessageWithTs = { ...validatedMessage, ts: Date.now() }
|
||||
this.apiConversationHistory.push(userMessageWithTs as ApiMessage)
|
||||
|
||||
|
|
@ -1573,14 +1588,51 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
// Get condensing configuration
|
||||
const state = await this.providerRef.deref()?.getState()
|
||||
const customCondensingPrompt = state?.customSupportPrompts?.CONDENSE
|
||||
const { mode, apiConfiguration } = state ?? {}
|
||||
|
||||
const { contextTokens: prevContextTokens } = this.getTokenUsage()
|
||||
|
||||
// Build tools for condensing metadata (same tools used for normal API calls)
|
||||
const provider = this.providerRef.deref()
|
||||
let allTools: import("openai").default.Chat.ChatCompletionTool[] = []
|
||||
if (provider) {
|
||||
const modelInfo = this.api.getModel().info
|
||||
const toolsResult = await buildNativeToolsArrayWithRestrictions({
|
||||
provider,
|
||||
cwd: this.cwd,
|
||||
mode,
|
||||
customModes: state?.customModes,
|
||||
experiments: state?.experiments,
|
||||
apiConfiguration,
|
||||
maxReadFileLine: state?.maxReadFileLine ?? -1,
|
||||
maxConcurrentFileReads: state?.maxConcurrentFileReads ?? 5,
|
||||
browserToolEnabled: state?.browserToolEnabled ?? true,
|
||||
modelInfo,
|
||||
diffEnabled: this.diffEnabled,
|
||||
includeAllToolsWithRestrictions: false,
|
||||
})
|
||||
allTools = toolsResult.tools
|
||||
}
|
||||
|
||||
// Build metadata with tools and taskId for the condensing API call
|
||||
const metadata: ApiHandlerCreateMessageMetadata = {
|
||||
mode,
|
||||
taskId: this.taskId,
|
||||
...(allTools.length > 0
|
||||
? {
|
||||
tools: allTools,
|
||||
tool_choice: "auto",
|
||||
parallelToolCalls: false,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
const {
|
||||
messages,
|
||||
summary,
|
||||
cost,
|
||||
newContextTokens = 0,
|
||||
error,
|
||||
errorDetails,
|
||||
condenseId,
|
||||
} = await summarizeConversation(
|
||||
this.apiConversationHistory,
|
||||
|
|
@ -1590,9 +1642,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
prevContextTokens,
|
||||
false, // manual trigger
|
||||
customCondensingPrompt, // User's custom prompt
|
||||
metadata, // Pass metadata with tools
|
||||
)
|
||||
if (error) {
|
||||
this.say(
|
||||
await this.say(
|
||||
"condense_context_error",
|
||||
error,
|
||||
undefined /* images */,
|
||||
|
|
@ -3659,7 +3712,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
|
||||
private async handleContextWindowExceededError(): Promise<void> {
|
||||
const state = await this.providerRef.deref()?.getState()
|
||||
const { profileThresholds = {} } = state ?? {}
|
||||
const { profileThresholds = {}, mode, apiConfiguration } = state ?? {}
|
||||
|
||||
const { contextTokens } = this.getTokenUsage()
|
||||
const modelInfo = this.api.getModel().info
|
||||
|
|
@ -3684,61 +3737,101 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
// Send condenseTaskContextStarted to show in-progress indicator
|
||||
await this.providerRef.deref()?.postMessageToWebview({ type: "condenseTaskContextStarted", text: this.taskId })
|
||||
|
||||
// Force aggressive truncation by keeping only 75% of the conversation history
|
||||
const truncateResult = await manageContext({
|
||||
messages: this.apiConversationHistory,
|
||||
totalTokens: contextTokens || 0,
|
||||
maxTokens,
|
||||
contextWindow,
|
||||
apiHandler: this.api,
|
||||
autoCondenseContext: true,
|
||||
autoCondenseContextPercent: FORCED_CONTEXT_REDUCTION_PERCENT,
|
||||
systemPrompt: await this.getSystemPrompt(),
|
||||
// Build tools for condensing metadata (same tools used for normal API calls)
|
||||
const provider = this.providerRef.deref()
|
||||
let allTools: import("openai").default.Chat.ChatCompletionTool[] = []
|
||||
if (provider) {
|
||||
const toolsResult = await buildNativeToolsArrayWithRestrictions({
|
||||
provider,
|
||||
cwd: this.cwd,
|
||||
mode,
|
||||
customModes: state?.customModes,
|
||||
experiments: state?.experiments,
|
||||
apiConfiguration,
|
||||
maxReadFileLine: state?.maxReadFileLine ?? -1,
|
||||
maxConcurrentFileReads: state?.maxConcurrentFileReads ?? 5,
|
||||
browserToolEnabled: state?.browserToolEnabled ?? true,
|
||||
modelInfo,
|
||||
diffEnabled: this.diffEnabled,
|
||||
includeAllToolsWithRestrictions: false,
|
||||
})
|
||||
allTools = toolsResult.tools
|
||||
}
|
||||
|
||||
// Build metadata with tools and taskId for the condensing API call
|
||||
const metadata: ApiHandlerCreateMessageMetadata = {
|
||||
mode,
|
||||
taskId: this.taskId,
|
||||
profileThresholds,
|
||||
currentProfileId,
|
||||
})
|
||||
|
||||
if (truncateResult.messages !== this.apiConversationHistory) {
|
||||
await this.overwriteApiConversationHistory(truncateResult.messages)
|
||||
...(allTools.length > 0
|
||||
? {
|
||||
tools: allTools,
|
||||
tool_choice: "auto",
|
||||
parallelToolCalls: false,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
|
||||
if (truncateResult.summary) {
|
||||
const { summary, cost, prevContextTokens, newContextTokens = 0 } = truncateResult
|
||||
const contextCondense: ContextCondense = { summary, cost, newContextTokens, prevContextTokens }
|
||||
await this.say(
|
||||
"condense_context",
|
||||
undefined /* text */,
|
||||
undefined /* images */,
|
||||
false /* partial */,
|
||||
undefined /* checkpoint */,
|
||||
undefined /* progressStatus */,
|
||||
{ isNonInteractive: true } /* options */,
|
||||
contextCondense,
|
||||
)
|
||||
} else if (truncateResult.truncationId) {
|
||||
// Sliding window truncation occurred (fallback when condensing fails or is disabled)
|
||||
const contextTruncation: ContextTruncation = {
|
||||
truncationId: truncateResult.truncationId,
|
||||
messagesRemoved: truncateResult.messagesRemoved ?? 0,
|
||||
prevContextTokens: truncateResult.prevContextTokens,
|
||||
newContextTokens: truncateResult.newContextTokensAfterTruncation ?? 0,
|
||||
try {
|
||||
// Force aggressive truncation by keeping only 75% of the conversation history
|
||||
const truncateResult = await manageContext({
|
||||
messages: this.apiConversationHistory,
|
||||
totalTokens: contextTokens || 0,
|
||||
maxTokens,
|
||||
contextWindow,
|
||||
apiHandler: this.api,
|
||||
autoCondenseContext: true,
|
||||
autoCondenseContextPercent: FORCED_CONTEXT_REDUCTION_PERCENT,
|
||||
systemPrompt: await this.getSystemPrompt(),
|
||||
taskId: this.taskId,
|
||||
profileThresholds,
|
||||
currentProfileId,
|
||||
metadata,
|
||||
})
|
||||
|
||||
if (truncateResult.messages !== this.apiConversationHistory) {
|
||||
await this.overwriteApiConversationHistory(truncateResult.messages)
|
||||
}
|
||||
await this.say(
|
||||
"sliding_window_truncation",
|
||||
undefined /* text */,
|
||||
undefined /* images */,
|
||||
false /* partial */,
|
||||
undefined /* checkpoint */,
|
||||
undefined /* progressStatus */,
|
||||
{ isNonInteractive: true } /* options */,
|
||||
undefined /* contextCondense */,
|
||||
contextTruncation,
|
||||
)
|
||||
}
|
||||
|
||||
// Notify webview that context management is complete (removes in-progress spinner)
|
||||
await this.providerRef.deref()?.postMessageToWebview({ type: "condenseTaskContextResponse", text: this.taskId })
|
||||
if (truncateResult.summary) {
|
||||
const { summary, cost, prevContextTokens, newContextTokens = 0 } = truncateResult
|
||||
const contextCondense: ContextCondense = { summary, cost, newContextTokens, prevContextTokens }
|
||||
await this.say(
|
||||
"condense_context",
|
||||
undefined /* text */,
|
||||
undefined /* images */,
|
||||
false /* partial */,
|
||||
undefined /* checkpoint */,
|
||||
undefined /* progressStatus */,
|
||||
{ isNonInteractive: true } /* options */,
|
||||
contextCondense,
|
||||
)
|
||||
} else if (truncateResult.truncationId) {
|
||||
// Sliding window truncation occurred (fallback when condensing fails or is disabled)
|
||||
const contextTruncation: ContextTruncation = {
|
||||
truncationId: truncateResult.truncationId,
|
||||
messagesRemoved: truncateResult.messagesRemoved ?? 0,
|
||||
prevContextTokens: truncateResult.prevContextTokens,
|
||||
newContextTokens: truncateResult.newContextTokensAfterTruncation ?? 0,
|
||||
}
|
||||
await this.say(
|
||||
"sliding_window_truncation",
|
||||
undefined /* text */,
|
||||
undefined /* images */,
|
||||
false /* partial */,
|
||||
undefined /* checkpoint */,
|
||||
undefined /* progressStatus */,
|
||||
{ isNonInteractive: true } /* options */,
|
||||
undefined /* contextCondense */,
|
||||
contextTruncation,
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
// Notify webview that context management is complete (removes in-progress spinner)
|
||||
// IMPORTANT: Must always be sent to dismiss the spinner, even on error
|
||||
await this.providerRef
|
||||
.deref()
|
||||
?.postMessageToWebview({ type: "condenseTaskContextResponse", text: this.taskId })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -3855,71 +3948,112 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
?.postMessageToWebview({ type: "condenseTaskContextStarted", text: this.taskId })
|
||||
}
|
||||
|
||||
const truncateResult = await manageContext({
|
||||
messages: this.apiConversationHistory,
|
||||
totalTokens: contextTokens,
|
||||
maxTokens,
|
||||
contextWindow,
|
||||
apiHandler: this.api,
|
||||
autoCondenseContext,
|
||||
autoCondenseContextPercent,
|
||||
systemPrompt,
|
||||
taskId: this.taskId,
|
||||
customCondensingPrompt,
|
||||
profileThresholds,
|
||||
currentProfileId,
|
||||
})
|
||||
if (truncateResult.messages !== this.apiConversationHistory) {
|
||||
await this.overwriteApiConversationHistory(truncateResult.messages)
|
||||
}
|
||||
if (truncateResult.error) {
|
||||
await this.say("condense_context_error", truncateResult.error)
|
||||
} else if (truncateResult.summary) {
|
||||
const { summary, cost, prevContextTokens, newContextTokens = 0, condenseId } = truncateResult
|
||||
const contextCondense: ContextCondense = {
|
||||
summary,
|
||||
cost,
|
||||
newContextTokens,
|
||||
prevContextTokens,
|
||||
condenseId,
|
||||
// Build tools for condensing metadata (same tools used for normal API calls)
|
||||
// This ensures the condensing API call includes tool definitions for providers that need them
|
||||
let contextMgmtTools: import("openai").default.Chat.ChatCompletionTool[] = []
|
||||
{
|
||||
const provider = this.providerRef.deref()
|
||||
if (provider) {
|
||||
const toolsResult = await buildNativeToolsArrayWithRestrictions({
|
||||
provider,
|
||||
cwd: this.cwd,
|
||||
mode,
|
||||
customModes: state?.customModes,
|
||||
experiments: state?.experiments,
|
||||
apiConfiguration,
|
||||
maxReadFileLine: state?.maxReadFileLine ?? -1,
|
||||
maxConcurrentFileReads: state?.maxConcurrentFileReads ?? 5,
|
||||
browserToolEnabled: state?.browserToolEnabled ?? true,
|
||||
modelInfo,
|
||||
diffEnabled: this.diffEnabled,
|
||||
includeAllToolsWithRestrictions: false,
|
||||
})
|
||||
contextMgmtTools = toolsResult.tools
|
||||
}
|
||||
await this.say(
|
||||
"condense_context",
|
||||
undefined /* text */,
|
||||
undefined /* images */,
|
||||
false /* partial */,
|
||||
undefined /* checkpoint */,
|
||||
undefined /* progressStatus */,
|
||||
{ isNonInteractive: true } /* options */,
|
||||
contextCondense,
|
||||
)
|
||||
} else if (truncateResult.truncationId) {
|
||||
// Sliding window truncation occurred (fallback when condensing fails or is disabled)
|
||||
const contextTruncation: ContextTruncation = {
|
||||
truncationId: truncateResult.truncationId,
|
||||
messagesRemoved: truncateResult.messagesRemoved ?? 0,
|
||||
prevContextTokens: truncateResult.prevContextTokens,
|
||||
newContextTokens: truncateResult.newContextTokensAfterTruncation ?? 0,
|
||||
}
|
||||
await this.say(
|
||||
"sliding_window_truncation",
|
||||
undefined /* text */,
|
||||
undefined /* images */,
|
||||
false /* partial */,
|
||||
undefined /* checkpoint */,
|
||||
undefined /* progressStatus */,
|
||||
{ isNonInteractive: true } /* options */,
|
||||
undefined /* contextCondense */,
|
||||
contextTruncation,
|
||||
)
|
||||
}
|
||||
|
||||
// Notify webview that context management is complete (sets isCondensing = false)
|
||||
// This removes the in-progress spinner and allows the completed result to show
|
||||
if (contextManagementWillRun && autoCondenseContext) {
|
||||
await this.providerRef
|
||||
.deref()
|
||||
?.postMessageToWebview({ type: "condenseTaskContextResponse", text: this.taskId })
|
||||
// Build metadata with tools and taskId for the condensing API call
|
||||
const contextMgmtMetadata: ApiHandlerCreateMessageMetadata = {
|
||||
mode,
|
||||
taskId: this.taskId,
|
||||
...(contextMgmtTools.length > 0
|
||||
? {
|
||||
tools: contextMgmtTools,
|
||||
tool_choice: "auto",
|
||||
parallelToolCalls: false,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
|
||||
try {
|
||||
const truncateResult = await manageContext({
|
||||
messages: this.apiConversationHistory,
|
||||
totalTokens: contextTokens,
|
||||
maxTokens,
|
||||
contextWindow,
|
||||
apiHandler: this.api,
|
||||
autoCondenseContext,
|
||||
autoCondenseContextPercent,
|
||||
systemPrompt,
|
||||
taskId: this.taskId,
|
||||
customCondensingPrompt,
|
||||
profileThresholds,
|
||||
currentProfileId,
|
||||
metadata: contextMgmtMetadata,
|
||||
})
|
||||
if (truncateResult.messages !== this.apiConversationHistory) {
|
||||
await this.overwriteApiConversationHistory(truncateResult.messages)
|
||||
}
|
||||
if (truncateResult.error) {
|
||||
await this.say("condense_context_error", truncateResult.error)
|
||||
} else if (truncateResult.summary) {
|
||||
const { summary, cost, prevContextTokens, newContextTokens = 0, condenseId } = truncateResult
|
||||
const contextCondense: ContextCondense = {
|
||||
summary,
|
||||
cost,
|
||||
newContextTokens,
|
||||
prevContextTokens,
|
||||
condenseId,
|
||||
}
|
||||
await this.say(
|
||||
"condense_context",
|
||||
undefined /* text */,
|
||||
undefined /* images */,
|
||||
false /* partial */,
|
||||
undefined /* checkpoint */,
|
||||
undefined /* progressStatus */,
|
||||
{ isNonInteractive: true } /* options */,
|
||||
contextCondense,
|
||||
)
|
||||
} else if (truncateResult.truncationId) {
|
||||
// Sliding window truncation occurred (fallback when condensing fails or is disabled)
|
||||
const contextTruncation: ContextTruncation = {
|
||||
truncationId: truncateResult.truncationId,
|
||||
messagesRemoved: truncateResult.messagesRemoved ?? 0,
|
||||
prevContextTokens: truncateResult.prevContextTokens,
|
||||
newContextTokens: truncateResult.newContextTokensAfterTruncation ?? 0,
|
||||
}
|
||||
await this.say(
|
||||
"sliding_window_truncation",
|
||||
undefined /* text */,
|
||||
undefined /* images */,
|
||||
false /* partial */,
|
||||
undefined /* checkpoint */,
|
||||
undefined /* progressStatus */,
|
||||
{ isNonInteractive: true } /* options */,
|
||||
undefined /* contextCondense */,
|
||||
contextTruncation,
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
// Notify webview that context management is complete (sets isCondensing = false)
|
||||
// This removes the in-progress spinner and allows the completed result to show
|
||||
// IMPORTANT: Must always be sent to dismiss the spinner, even on error
|
||||
if (contextManagementWillRun && autoCondenseContext) {
|
||||
await this.providerRef
|
||||
.deref()
|
||||
?.postMessageToWebview({ type: "condenseTaskContextResponse", text: this.taskId })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3928,7 +4062,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
// enabling accurate rewind operations while still sending condensed history to the API.
|
||||
const effectiveHistory = getEffectiveApiHistory(this.apiConversationHistory)
|
||||
const messagesSinceLastSummary = getMessagesSinceLastSummary(effectiveHistory)
|
||||
const messagesWithoutImages = maybeRemoveImageBlocks(messagesSinceLastSummary, this.api)
|
||||
// For API only: merge consecutive user messages (excludes summary messages per
|
||||
// mergeConsecutiveApiMessages implementation) without mutating stored history.
|
||||
const mergedForApi = mergeConsecutiveApiMessages(messagesSinceLastSummary, { roles: ["user"] })
|
||||
const messagesWithoutImages = maybeRemoveImageBlocks(mergedForApi, this.api)
|
||||
const cleanConversationHistory = this.buildCleanConversationHistory(messagesWithoutImages as ApiMessage[])
|
||||
|
||||
// Check auto-approval limits
|
||||
|
|
|
|||
46
src/core/task/__tests__/mergeConsecutiveApiMessages.spec.ts
Normal file
46
src/core/task/__tests__/mergeConsecutiveApiMessages.spec.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// npx vitest run core/task/__tests__/mergeConsecutiveApiMessages.spec.ts
|
||||
|
||||
import { mergeConsecutiveApiMessages } from "../mergeConsecutiveApiMessages"
|
||||
|
||||
describe("mergeConsecutiveApiMessages", () => {
|
||||
it("merges consecutive user messages by default", () => {
|
||||
const merged = mergeConsecutiveApiMessages([
|
||||
{ role: "user", content: "A", ts: 1 },
|
||||
{ role: "user", content: [{ type: "text", text: "B" }], ts: 2 },
|
||||
{ role: "assistant", content: "C", ts: 3 },
|
||||
])
|
||||
|
||||
expect(merged).toHaveLength(2)
|
||||
expect(merged[0].role).toBe("user")
|
||||
expect(merged[0].content).toEqual([
|
||||
{ type: "text", text: "A" },
|
||||
{ type: "text", text: "B" },
|
||||
])
|
||||
expect(merged[1].role).toBe("assistant")
|
||||
})
|
||||
|
||||
it("merges regular user message into a summary (API shaping only)", () => {
|
||||
const merged = mergeConsecutiveApiMessages([
|
||||
{ role: "user", content: [{ type: "text", text: "Summary" }], ts: 1, isSummary: true, condenseId: "s" },
|
||||
{ role: "user", content: [{ type: "text", text: "After" }], ts: 2 },
|
||||
])
|
||||
|
||||
expect(merged).toHaveLength(1)
|
||||
expect(merged[0].isSummary).toBe(true)
|
||||
expect(merged[0].content).toEqual([
|
||||
{ type: "text", text: "Summary" },
|
||||
{ type: "text", text: "After" },
|
||||
])
|
||||
})
|
||||
|
||||
it("does not merge a summary into a preceding message", () => {
|
||||
const merged = mergeConsecutiveApiMessages([
|
||||
{ role: "user", content: [{ type: "text", text: "Before" }], ts: 1 },
|
||||
{ role: "user", content: [{ type: "text", text: "Summary" }], ts: 2, isSummary: true, condenseId: "s" },
|
||||
])
|
||||
|
||||
expect(merged).toHaveLength(2)
|
||||
expect(merged[0].isSummary).toBeUndefined()
|
||||
expect(merged[1].isSummary).toBe(true)
|
||||
})
|
||||
})
|
||||
59
src/core/task/mergeConsecutiveApiMessages.ts
Normal file
59
src/core/task/mergeConsecutiveApiMessages.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
|
||||
import type { ApiMessage } from "../task-persistence"
|
||||
|
||||
type Role = ApiMessage["role"]
|
||||
|
||||
function normalizeContentToBlocks(content: ApiMessage["content"]): Anthropic.Messages.ContentBlockParam[] {
|
||||
if (Array.isArray(content)) {
|
||||
return content as Anthropic.Messages.ContentBlockParam[]
|
||||
}
|
||||
if (content === undefined || content === null) {
|
||||
return []
|
||||
}
|
||||
return [{ type: "text", text: String(content) }]
|
||||
}
|
||||
|
||||
/**
|
||||
* Non-destructively merges consecutive messages with the same role.
|
||||
*
|
||||
* Used for *API request shaping only* (do not use for storage), so rewind/edit operations
|
||||
* can still reference the original individual messages.
|
||||
*/
|
||||
export function mergeConsecutiveApiMessages(messages: ApiMessage[], options?: { roles?: Role[] }): ApiMessage[] {
|
||||
if (messages.length <= 1) {
|
||||
return messages
|
||||
}
|
||||
|
||||
const mergeRoles = new Set<Role>(options?.roles ?? ["user"]) // default: user only
|
||||
const out: ApiMessage[] = []
|
||||
|
||||
for (const msg of messages) {
|
||||
const prev = out[out.length - 1]
|
||||
const canMerge =
|
||||
prev &&
|
||||
prev.role === msg.role &&
|
||||
mergeRoles.has(msg.role) &&
|
||||
// Allow merging regular messages into a summary (API-only shaping),
|
||||
// but never merge a summary into something else.
|
||||
!msg.isSummary &&
|
||||
!prev.isTruncationMarker &&
|
||||
!msg.isTruncationMarker
|
||||
|
||||
if (!canMerge) {
|
||||
out.push(msg)
|
||||
continue
|
||||
}
|
||||
|
||||
const mergedContent = [...normalizeContentToBlocks(prev.content), ...normalizeContentToBlocks(msg.content)]
|
||||
|
||||
// Preserve the newest ts to keep chronological ordering for downstream logic.
|
||||
out[out.length - 1] = {
|
||||
...prev,
|
||||
content: mergedContent,
|
||||
ts: Math.max(prev.ts ?? 0, msg.ts ?? 0) || prev.ts || msg.ts,
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
|
@ -264,7 +264,7 @@ describe("webviewMessageHandler delete functionality", () => {
|
|||
|
||||
// API history after condense: msg1, msg2(tagged), msg3(tagged), summary, kept1, kept2, kept3
|
||||
getCurrentTaskMock.apiConversationHistory = [
|
||||
{ ts: 100, role: "user", content: "First message" },
|
||||
{ ts: 100, role: "user", content: "First message", condenseParent: condenseId },
|
||||
{ ts: 200, role: "assistant", content: "Response 1", condenseParent: condenseId },
|
||||
{ ts: 300, role: "user", content: "Second message", condenseParent: condenseId },
|
||||
{ ts: 799, role: "assistant", content: "Summary", isSummary: true, condenseId },
|
||||
|
|
@ -286,6 +286,7 @@ describe("webviewMessageHandler delete functionality", () => {
|
|||
// Expected: [msg1, msg2(tagged), msg3(tagged), summary, kept1]
|
||||
expect(result.length).toBe(5)
|
||||
expect(result[0].content).toBe("First message")
|
||||
expect(result[0].condenseParent).toBe(condenseId) // Tag preserved
|
||||
expect(result[1].content).toBe("Response 1")
|
||||
expect(result[1].condenseParent).toBe(condenseId) // Tag preserved
|
||||
expect(result[2].content).toBe("Second message")
|
||||
|
|
@ -310,7 +311,7 @@ describe("webviewMessageHandler delete functionality", () => {
|
|||
|
||||
// API history with condensed messages and summary
|
||||
getCurrentTaskMock.apiConversationHistory = [
|
||||
{ ts: 100, role: "user", content: "Task start" },
|
||||
{ ts: 100, role: "user", content: "Task start", condenseParent: condenseId },
|
||||
{ ts: 200, role: "assistant", content: "Response 1", condenseParent: condenseId },
|
||||
{ ts: 300, role: "user", content: "Message 2", condenseParent: condenseId },
|
||||
{ ts: 999, role: "assistant", content: "Summary", isSummary: true, condenseId },
|
||||
|
|
@ -349,7 +350,7 @@ describe("webviewMessageHandler delete functionality", () => {
|
|||
]
|
||||
|
||||
getCurrentTaskMock.apiConversationHistory = [
|
||||
{ ts: 100, role: "user", content: "First message" },
|
||||
{ ts: 100, role: "user", content: "First message", condenseParent: condenseId1 },
|
||||
// Messages from first condense (tagged with condenseId1)
|
||||
{ ts: 200, role: "assistant", content: "Msg2", condenseParent: condenseId1 },
|
||||
{ ts: 300, role: "user", content: "Msg3", condenseParent: condenseId1 },
|
||||
|
|
@ -446,7 +447,7 @@ describe("webviewMessageHandler delete functionality", () => {
|
|||
// Summary has ts=299 (before first kept message), so it would survive basic truncation
|
||||
// But since condense_context (ts=500) is being removed, Summary should be removed too
|
||||
getCurrentTaskMock.apiConversationHistory = [
|
||||
{ ts: 100, role: "user", content: "Task start" },
|
||||
{ ts: 100, role: "user", content: "Task start", condenseParent: condenseId },
|
||||
{ ts: 200, role: "assistant", content: "Response 1", condenseParent: condenseId },
|
||||
// Summary timestamp is BEFORE the kept messages (this is the bug scenario)
|
||||
{ ts: 299, role: "assistant", content: "Summary text", isSummary: true, condenseId },
|
||||
|
|
@ -503,7 +504,7 @@ describe("webviewMessageHandler delete functionality", () => {
|
|||
]
|
||||
|
||||
getCurrentTaskMock.apiConversationHistory = [
|
||||
{ ts: 100, role: "user", content: "First message" },
|
||||
{ ts: 100, role: "user", content: "First message", condenseParent: condenseId1 },
|
||||
// Messages from first condense (tagged with condenseId1)
|
||||
{ ts: 200, role: "assistant", content: "Response 1", condenseParent: condenseId1 },
|
||||
// First summary (also tagged with condenseId2 from second condense)
|
||||
|
|
|
|||
1
src/i18n/locales/ca/common.json
generated
1
src/i18n/locales/ca/common.json
generated
|
|
@ -67,6 +67,7 @@
|
|||
"condensed_recently": "El context s'ha condensat recentment; s'omet aquest intent",
|
||||
"condense_handler_invalid": "El gestor de l'API per condensar el context no és vàlid",
|
||||
"condense_context_grew": "La mida del context ha augmentat durant la condensació; s'omet aquest intent",
|
||||
"condense_api_failed": "La crida a l'API de condensació ha fallat: {{message}}",
|
||||
"url_timeout": "El lloc web ha trigat massa a carregar (timeout). Això pot ser degut a una connexió lenta, un lloc web pesat o temporalment no disponible. Pots tornar-ho a provar més tard o comprovar si la URL és correcta.",
|
||||
"url_not_found": "No s'ha pogut trobar l'adreça del lloc web. Comprova si la URL és correcta i torna-ho a provar.",
|
||||
"no_internet": "No hi ha connexió a internet. Comprova la teva connexió de xarxa i torna-ho a provar.",
|
||||
|
|
|
|||
1
src/i18n/locales/de/common.json
generated
1
src/i18n/locales/de/common.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"condensed_recently": "Kontext wurde kürzlich verdichtet; dieser Versuch wird übersprungen",
|
||||
"condense_handler_invalid": "API-Handler zum Verdichten des Kontexts ist ungültig",
|
||||
"condense_context_grew": "Kontextgröße ist während der Verdichtung gewachsen; dieser Versuch wird übersprungen",
|
||||
"condense_api_failed": "Verdichtungs-API-Aufruf fehlgeschlagen: {{message}}",
|
||||
"url_timeout": "Die Website hat zu lange zum Laden gebraucht (Timeout). Das könnte an einer langsamen Verbindung, einer schweren Website oder vorübergehender Nichtverfügbarkeit liegen. Du kannst es später nochmal versuchen oder prüfen, ob die URL korrekt ist.",
|
||||
"url_not_found": "Die Website-Adresse konnte nicht gefunden werden. Bitte prüfe, ob die URL korrekt ist und versuche es erneut.",
|
||||
"no_internet": "Keine Internetverbindung. Bitte prüfe deine Netzwerkverbindung und versuche es erneut.",
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
"condensed_recently": "Context was condensed recently; skipping this attempt",
|
||||
"condense_handler_invalid": "API handler for condensing context is invalid",
|
||||
"condense_context_grew": "Context size increased during condensing; skipping this attempt",
|
||||
"condense_api_failed": "Condensing API call failed: {{message}}",
|
||||
"url_timeout": "The website took too long to load (timeout). This could be due to a slow connection, heavy website, or the site being temporarily unavailable. You can try again later or check if the URL is correct.",
|
||||
"url_not_found": "The website address could not be found. Please check if the URL is correct and try again.",
|
||||
"no_internet": "No internet connection. Please check your network connection and try again.",
|
||||
|
|
|
|||
1
src/i18n/locales/es/common.json
generated
1
src/i18n/locales/es/common.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"condensed_recently": "El contexto se condensó recientemente; se omite este intento",
|
||||
"condense_handler_invalid": "El manejador de API para condensar el contexto no es válido",
|
||||
"condense_context_grew": "El tamaño del contexto aumentó durante la condensación; se omite este intento",
|
||||
"condense_api_failed": "La llamada API de condensación falló: {{message}}",
|
||||
"url_timeout": "El sitio web tardó demasiado en cargar (timeout). Esto podría deberse a una conexión lenta, un sitio web pesado o que esté temporalmente no disponible. Puedes intentarlo más tarde o verificar si la URL es correcta.",
|
||||
"url_not_found": "No se pudo encontrar la dirección del sitio web. Por favor verifica si la URL es correcta e inténtalo de nuevo.",
|
||||
"no_internet": "Sin conexión a internet. Por favor verifica tu conexión de red e inténtalo de nuevo.",
|
||||
|
|
|
|||
1
src/i18n/locales/fr/common.json
generated
1
src/i18n/locales/fr/common.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"condensed_recently": "Le contexte a été condensé récemment ; cette tentative est ignorée",
|
||||
"condense_handler_invalid": "Le gestionnaire d'API pour condenser le contexte est invalide",
|
||||
"condense_context_grew": "La taille du contexte a augmenté pendant la condensation ; cette tentative est ignorée",
|
||||
"condense_api_failed": "L'appel API de condensation a échoué : {{message}}",
|
||||
"url_timeout": "Le site web a pris trop de temps à charger (timeout). Cela pourrait être dû à une connexion lente, un site web lourd ou temporairement indisponible. Tu peux réessayer plus tard ou vérifier si l'URL est correcte.",
|
||||
"url_not_found": "L'adresse du site web n'a pas pu être trouvée. Vérifie si l'URL est correcte et réessaie.",
|
||||
"no_internet": "Pas de connexion internet. Vérifie ta connexion réseau et réessaie.",
|
||||
|
|
|
|||
1
src/i18n/locales/hi/common.json
generated
1
src/i18n/locales/hi/common.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"condensed_recently": "संदर्भ हाल ही में संक्षिप्त किया गया था; इस प्रयास को छोड़ा जा रहा है",
|
||||
"condense_handler_invalid": "संदर्भ को संक्षिप्त करने के लिए API हैंडलर अमान्य है",
|
||||
"condense_context_grew": "संक्षिप्तीकरण के दौरान संदर्भ का आकार बढ़ गया; इस प्रयास को छोड़ा जा रहा है",
|
||||
"condense_api_failed": "संक्षिप्तीकरण API कॉल विफल: {{message}}",
|
||||
"url_timeout": "वेबसाइट लोड होने में बहुत समय लगा (टाइमआउट)। यह धीमे कनेक्शन, भारी वेबसाइट या अस्थायी रूप से अनुपलब्ध होने के कारण हो सकता है। आप बाद में फिर से कोशिश कर सकते हैं या जांच सकते हैं कि URL सही है या नहीं।",
|
||||
"url_not_found": "वेबसाइट का पता नहीं मिल सका। कृपया जांचें कि URL सही है और फिर से कोशिश करें।",
|
||||
"no_internet": "इंटरनेट कनेक्शन नहीं है। कृपया अपना नेटवर्क कनेक्शन जांचें और फिर से कोशिश करें।",
|
||||
|
|
|
|||
1
src/i18n/locales/id/common.json
generated
1
src/i18n/locales/id/common.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"condensed_recently": "Konteks baru saja dikompres; melewati percobaan ini",
|
||||
"condense_handler_invalid": "Handler API untuk mengompres konteks tidak valid",
|
||||
"condense_context_grew": "Ukuran konteks bertambah saat mengompres; melewati percobaan ini",
|
||||
"condense_api_failed": "Panggilan API pengompresan gagal: {{message}}",
|
||||
"url_timeout": "Situs web membutuhkan waktu terlalu lama untuk dimuat (timeout). Ini bisa disebabkan oleh koneksi lambat, situs web berat, atau sementara tidak tersedia. Kamu bisa mencoba lagi nanti atau memeriksa apakah URL sudah benar.",
|
||||
"url_not_found": "Alamat situs web tidak dapat ditemukan. Silakan periksa apakah URL sudah benar dan coba lagi.",
|
||||
"no_internet": "Tidak ada koneksi internet. Silakan periksa koneksi jaringan kamu dan coba lagi.",
|
||||
|
|
|
|||
1
src/i18n/locales/it/common.json
generated
1
src/i18n/locales/it/common.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"condensed_recently": "Il contesto è stato condensato di recente; questo tentativo viene saltato",
|
||||
"condense_handler_invalid": "Il gestore API per condensare il contesto non è valido",
|
||||
"condense_context_grew": "La dimensione del contesto è aumentata durante la condensazione; questo tentativo viene saltato",
|
||||
"condense_api_failed": "Chiamata API di condensazione fallita: {{message}}",
|
||||
"url_timeout": "Il sito web ha impiegato troppo tempo a caricarsi (timeout). Questo potrebbe essere dovuto a una connessione lenta, un sito web pesante o temporaneamente non disponibile. Puoi riprovare più tardi o verificare se l'URL è corretto.",
|
||||
"url_not_found": "L'indirizzo del sito web non è stato trovato. Verifica se l'URL è corretto e riprova.",
|
||||
"no_internet": "Nessuna connessione internet. Verifica la tua connessione di rete e riprova.",
|
||||
|
|
|
|||
1
src/i18n/locales/ja/common.json
generated
1
src/i18n/locales/ja/common.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"condensed_recently": "コンテキストは最近圧縮されました;この試行をスキップします",
|
||||
"condense_handler_invalid": "コンテキストを圧縮するためのAPIハンドラーが無効です",
|
||||
"condense_context_grew": "圧縮中にコンテキストサイズが増加しました;この試行をスキップします",
|
||||
"condense_api_failed": "圧縮API呼び出しが失敗しました:{{message}}",
|
||||
"url_timeout": "ウェブサイトの読み込みがタイムアウトしました。接続が遅い、ウェブサイトが重い、または一時的に利用できない可能性があります。後でもう一度試すか、URLが正しいか確認してください。",
|
||||
"url_not_found": "ウェブサイトのアドレスが見つかりませんでした。URLが正しいか確認してもう一度試してください。",
|
||||
"no_internet": "インターネット接続がありません。ネットワーク接続を確認してもう一度試してください。",
|
||||
|
|
|
|||
1
src/i18n/locales/ko/common.json
generated
1
src/i18n/locales/ko/common.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"condensed_recently": "컨텍스트가 최근 압축되었습니다; 이 시도를 건너뜁니다",
|
||||
"condense_handler_invalid": "컨텍스트 압축을 위한 API 핸들러가 유효하지 않습니다",
|
||||
"condense_context_grew": "압축 중 컨텍스트 크기가 증가했습니다; 이 시도를 건너뜁니다",
|
||||
"condense_api_failed": "압축 API 호출 실패: {{message}}",
|
||||
"url_timeout": "웹사이트 로딩이 너무 오래 걸렸습니다(타임아웃). 느린 연결, 무거운 웹사이트 또는 일시적으로 사용할 수 없는 상태일 수 있습니다. 나중에 다시 시도하거나 URL이 올바른지 확인해 주세요.",
|
||||
"url_not_found": "웹사이트 주소를 찾을 수 없습니다. URL이 올바른지 확인하고 다시 시도해 주세요.",
|
||||
"no_internet": "인터넷 연결이 없습니다. 네트워크 연결을 확인하고 다시 시도해 주세요.",
|
||||
|
|
|
|||
1
src/i18n/locales/nl/common.json
generated
1
src/i18n/locales/nl/common.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"condensed_recently": "Context is recent gecomprimeerd; deze poging wordt overgeslagen",
|
||||
"condense_handler_invalid": "API-handler voor het comprimeren van context is ongeldig",
|
||||
"condense_context_grew": "Contextgrootte nam toe tijdens comprimeren; deze poging wordt overgeslagen",
|
||||
"condense_api_failed": "Comprimeer API-oproep mislukt: {{message}}",
|
||||
"url_timeout": "De website deed er te lang over om te laden (timeout). Dit kan komen door een trage verbinding, een zware website of tijdelijke onbeschikbaarheid. Je kunt het later opnieuw proberen of controleren of de URL correct is.",
|
||||
"url_not_found": "Het websiteadres kon niet worden gevonden. Controleer of de URL correct is en probeer opnieuw.",
|
||||
"no_internet": "Geen internetverbinding. Controleer je netwerkverbinding en probeer opnieuw.",
|
||||
|
|
|
|||
1
src/i18n/locales/pl/common.json
generated
1
src/i18n/locales/pl/common.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"condensed_recently": "Kontekst został niedawno skondensowany; pomijanie tej próby",
|
||||
"condense_handler_invalid": "Nieprawidłowy handler API do kondensowania kontekstu",
|
||||
"condense_context_grew": "Rozmiar kontekstu wzrósł podczas kondensacji; pomijanie tej próby",
|
||||
"condense_api_failed": "Wywołanie API kondensacji nie powiodło się: {{message}}",
|
||||
"url_timeout": "Strona internetowa ładowała się zbyt długo (timeout). Może to być spowodowane wolnym połączeniem, ciężką stroną lub tymczasową niedostępnością. Możesz spróbować ponownie później lub sprawdzić, czy URL jest poprawny.",
|
||||
"url_not_found": "Nie można znaleźć adresu strony internetowej. Sprawdź, czy URL jest poprawny i spróbuj ponownie.",
|
||||
"no_internet": "Brak połączenia z internetem. Sprawdź połączenie sieciowe i spróbuj ponownie.",
|
||||
|
|
|
|||
1
src/i18n/locales/pt-BR/common.json
generated
1
src/i18n/locales/pt-BR/common.json
generated
|
|
@ -67,6 +67,7 @@
|
|||
"condensed_recently": "O contexto foi condensado recentemente; pulando esta tentativa",
|
||||
"condense_handler_invalid": "O manipulador de API para condensar o contexto é inválido",
|
||||
"condense_context_grew": "O tamanho do contexto aumentou durante a condensação; pulando esta tentativa",
|
||||
"condense_api_failed": "Chamada de API de condensação falhou: {{message}}",
|
||||
"url_timeout": "O site demorou muito para carregar (timeout). Isso pode ser devido a uma conexão lenta, site pesado ou temporariamente indisponível. Você pode tentar novamente mais tarde ou verificar se a URL está correta.",
|
||||
"url_not_found": "O endereço do site não pôde ser encontrado. Verifique se a URL está correta e tente novamente.",
|
||||
"no_internet": "Sem conexão com a internet. Verifique sua conexão de rede e tente novamente.",
|
||||
|
|
|
|||
1
src/i18n/locales/ru/common.json
generated
1
src/i18n/locales/ru/common.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"condensed_recently": "Контекст был недавно сжат; пропускаем эту попытку",
|
||||
"condense_handler_invalid": "Обработчик API для сжатия контекста недействителен",
|
||||
"condense_context_grew": "Размер контекста увеличился во время сжатия; пропускаем эту попытку",
|
||||
"condense_api_failed": "Ошибка вызова API сжатия: {{message}}",
|
||||
"url_timeout": "Веб-сайт слишком долго загружался (таймаут). Это может быть из-за медленного соединения, тяжелого веб-сайта или временной недоступности. Ты можешь попробовать позже или проверить правильность URL.",
|
||||
"url_not_found": "Адрес веб-сайта не найден. Проверь правильность URL и попробуй снова.",
|
||||
"no_internet": "Нет подключения к интернету. Проверь сетевое подключение и попробуй снова.",
|
||||
|
|
|
|||
1
src/i18n/locales/tr/common.json
generated
1
src/i18n/locales/tr/common.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"condensed_recently": "Bağlam yakın zamanda sıkıştırıldı; bu deneme atlanıyor",
|
||||
"condense_handler_invalid": "Bağlamı sıkıştırmak için API işleyicisi geçersiz",
|
||||
"condense_context_grew": "Sıkıştırma sırasında bağlam boyutu arttı; bu deneme atlanıyor",
|
||||
"condense_api_failed": "Sıkıştırma API çağrısı başarısız oldu: {{message}}",
|
||||
"url_timeout": "Web sitesi yüklenmesi çok uzun sürdü (zaman aşımı). Bu yavaş bağlantı, ağır web sitesi veya geçici olarak kullanılamama nedeniyle olabilir. Daha sonra tekrar deneyebilir veya URL'nin doğru olup olmadığını kontrol edebilirsin.",
|
||||
"url_not_found": "Web sitesi adresi bulunamadı. URL'nin doğru olup olmadığını kontrol et ve tekrar dene.",
|
||||
"no_internet": "İnternet bağlantısı yok. Ağ bağlantını kontrol et ve tekrar dene.",
|
||||
|
|
|
|||
1
src/i18n/locales/vi/common.json
generated
1
src/i18n/locales/vi/common.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"condensed_recently": "Ngữ cảnh đã được nén gần đây; bỏ qua lần thử này",
|
||||
"condense_handler_invalid": "Trình xử lý API để nén ngữ cảnh không hợp lệ",
|
||||
"condense_context_grew": "Kích thước ngữ cảnh tăng lên trong quá trình nén; bỏ qua lần thử này",
|
||||
"condense_api_failed": "Cuộc gọi API nén thất bại: {{message}}",
|
||||
"url_timeout": "Trang web mất quá nhiều thời gian để tải (timeout). Điều này có thể do kết nối chậm, trang web nặng hoặc tạm thời không khả dụng. Bạn có thể thử lại sau hoặc kiểm tra xem URL có đúng không.",
|
||||
"url_not_found": "Không thể tìm thấy địa chỉ trang web. Vui lòng kiểm tra URL có đúng không và thử lại.",
|
||||
"no_internet": "Không có kết nối internet. Vui lòng kiểm tra kết nối mạng và thử lại.",
|
||||
|
|
|
|||
1
src/i18n/locales/zh-CN/common.json
generated
1
src/i18n/locales/zh-CN/common.json
generated
|
|
@ -68,6 +68,7 @@
|
|||
"condensed_recently": "上下文最近已压缩;跳过此次尝试",
|
||||
"condense_handler_invalid": "压缩上下文的API处理程序无效",
|
||||
"condense_context_grew": "压缩过程中上下文大小增加;跳过此次尝试",
|
||||
"condense_api_failed": "压缩 API 调用失败:{{message}}",
|
||||
"url_timeout": "网站加载超时。这可能是由于网络连接缓慢、网站负载过重或暂时不可用。你可以稍后重试或检查 URL 是否正确。",
|
||||
"url_not_found": "找不到网站地址。请检查 URL 是否正确并重试。",
|
||||
"no_internet": "无网络连接。请检查网络连接并重试。",
|
||||
|
|
|
|||
1
src/i18n/locales/zh-TW/common.json
generated
1
src/i18n/locales/zh-TW/common.json
generated
|
|
@ -63,6 +63,7 @@
|
|||
"condensed_recently": "上下文最近已壓縮;跳過此次嘗試",
|
||||
"condense_handler_invalid": "壓縮上下文的 API 處理程式無效",
|
||||
"condense_context_grew": "壓縮過程中上下文大小增加;跳過此次嘗試",
|
||||
"condense_api_failed": "壓縮 API 呼叫失敗:{{message}}",
|
||||
"url_timeout": "網站載入超時。這可能是由於網路連線緩慢、網站負載過重或暫時無法使用。你可以稍後重試或檢查 URL 是否正確。",
|
||||
"url_not_found": "找不到網站位址。請檢查 URL 是否正確並重試。",
|
||||
"no_internet": "無網路連線。請檢查網路連線並重試。",
|
||||
|
|
|
|||
|
|
@ -52,43 +52,109 @@ const supportPromptConfigs: Record<SupportPromptType, SupportPromptConfig> = {
|
|||
\${userInput}`,
|
||||
},
|
||||
CONDENSE: {
|
||||
template: `Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
|
||||
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing with the conversation and supporting any continuing tasks.
|
||||
template: `CRITICAL: This summarization request is a SYSTEM OPERATION, not a user message.
|
||||
When analyzing "user requests" and "user intent", completely EXCLUDE this summarization message.
|
||||
The "most recent user request" and "Optional Next Step" must be based on what the user was doing BEFORE this system message appeared.
|
||||
The goal is for work to continue seamlessly after condensation - as if it never happened.
|
||||
|
||||
Your summary should be structured as follows:
|
||||
Context: The context to continue the conversation with. If applicable based on the current task, this should include:
|
||||
1. Previous Conversation: High level details about what was discussed throughout the entire conversation with the user. This should be written to allow someone to be able to follow the general overarching conversation flow.
|
||||
2. Current Work: Describe in detail what was being worked on prior to this request to summarize the conversation. Pay special attention to the more recent messages in the conversation.
|
||||
3. Key Technical Concepts: List all important technical concepts, technologies, coding conventions, and frameworks discussed, which might be relevant for continuing with this work.
|
||||
4. Relevant Files and Code: If applicable, enumerate specific files and code sections examined, modified, or created for the task continuation. Pay special attention to the most recent messages and changes.
|
||||
5. Problem Solving: Document problems solved thus far and any ongoing troubleshooting efforts.
|
||||
6. Pending Tasks and Next Steps: Outline all pending tasks that you have explicitly been asked to work on, as well as list the next steps you will take for all outstanding work, if applicable. Include code snippets where they add clarity. For any next steps, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no information loss in context between tasks.
|
||||
Your task is to create a detailed summary of the conversation so far, paying close attention to the user's explicit requests and your previous actions.
|
||||
This summary should be thorough in capturing technical details, code patterns, and architectural decisions that would be essential for continuing development work without losing context.
|
||||
|
||||
Before providing your final summary, wrap your analysis in <analysis> tags to organize your thoughts and ensure you've covered all necessary points. In your analysis process:
|
||||
|
||||
1. Chronologically analyze each message and section of the conversation. For each section thoroughly identify:
|
||||
- The user's explicit requests and intents
|
||||
- Your approach to addressing the user's requests
|
||||
- Key decisions, technical concepts and code patterns
|
||||
- Specific details like:
|
||||
- file names
|
||||
- full code snippets
|
||||
- function signatures
|
||||
- file edits
|
||||
- Errors that you ran into and how you fixed them
|
||||
- Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
|
||||
2. Double-check for technical accuracy and completeness, addressing each required element thoroughly.
|
||||
|
||||
Your summary should include the following sections:
|
||||
|
||||
1. Primary Request and Intent: Capture all of the user's explicit requests and intents in detail
|
||||
2. Key Technical Concepts: List all important technical concepts, technologies, and frameworks discussed.
|
||||
3. Files and Code Sections: Enumerate specific files and code sections examined, modified, or created. Pay special attention to the most recent messages and include full code snippets where applicable and include a summary of why this file read or edit is important.
|
||||
4. Errors and fixes: List all errors that you ran into, and how you fixed them. Pay special attention to specific user feedback that you received, especially if the user told you to do something differently.
|
||||
5. Problem Solving: Document problems solved and any ongoing troubleshooting efforts.
|
||||
6. All user messages: List ALL user messages that are not tool results. These are critical for understanding the users' feedback and changing intent.
|
||||
7. Pending Tasks: Outline any pending tasks that you have explicitly been asked to work on.
|
||||
8. Current Work: Describe in detail precisely what was being worked on immediately before this summary request, paying special attention to the most recent messages from both user and assistant. Include file names and code snippets where applicable.
|
||||
9. Optional Next Step: List the next step that you will take that is related to the most recent work you were doing. IMPORTANT: ensure that this step is DIRECTLY in line with the user's most recent explicit requests, and the task you were working on immediately before this summary request. If your last task was concluded, then only list next steps if they are explicitly in line with the users request. Do not start on tangential requests or really old requests that were already completed without confirming with the user first.
|
||||
|
||||
If there is a next step, include direct quotes from the most recent conversation showing exactly what task you were working on and where you left off. This should be verbatim to ensure there's no drift in task interpretation.
|
||||
|
||||
Here's an example of how your output should be structured:
|
||||
|
||||
<example>
|
||||
<analysis>
|
||||
[Your thought process, ensuring all points are covered thoroughly and accurately]
|
||||
</analysis>
|
||||
|
||||
<summary>
|
||||
1. Primary Request and Intent:
|
||||
[Detailed description]
|
||||
|
||||
2. Key Technical Concepts:
|
||||
- [Concept 1]
|
||||
- [Concept 2]
|
||||
- [...]
|
||||
|
||||
3. Files and Code Sections:
|
||||
- [File Name 1]
|
||||
- [Summary of why this file is important]
|
||||
- [Summary of the changes made to this file, if any]
|
||||
- [Important Code Snippet]
|
||||
- [File Name 2]
|
||||
- [Important Code Snippet]
|
||||
- [...]
|
||||
|
||||
4. Errors and fixes:
|
||||
- [Detailed description of error 1]:
|
||||
- [How you fixed the error]
|
||||
- [User feedback on the error if any]
|
||||
- [...]
|
||||
|
||||
Example summary structure:
|
||||
1. Previous Conversation:
|
||||
[Detailed description]
|
||||
2. Current Work:
|
||||
[Detailed description]
|
||||
3. Key Technical Concepts:
|
||||
- [Concept 1]
|
||||
- [Concept 2]
|
||||
- [...]
|
||||
4. Relevant Files and Code:
|
||||
- [File Name 1]
|
||||
- [Summary of why this file is important]
|
||||
- [Summary of the changes made to this file, if any]
|
||||
- [Important Code Snippet]
|
||||
- [File Name 2]
|
||||
- [Important Code Snippet]
|
||||
- [...]
|
||||
5. Problem Solving:
|
||||
[Detailed description]
|
||||
6. Pending Tasks and Next Steps:
|
||||
- [Task 1 details & next steps]
|
||||
- [Task 2 details & next steps]
|
||||
- [...]
|
||||
[Description of solved problems and ongoing troubleshooting]
|
||||
|
||||
Output only the summary of the conversation so far, without any additional commentary or explanation.`,
|
||||
6. All user messages:
|
||||
- [Detailed non tool use user message]
|
||||
- [...]
|
||||
|
||||
7. Pending Tasks:
|
||||
- [Task 1]
|
||||
- [Task 2]
|
||||
- [...]
|
||||
|
||||
8. Current Work:
|
||||
[Precise description of current work]
|
||||
|
||||
9. Optional Next Step:
|
||||
[Optional Next step to take]
|
||||
|
||||
</summary>
|
||||
</example>
|
||||
|
||||
Please provide your summary based on the conversation so far, following this structure and ensuring precision and thoroughness in your response.
|
||||
|
||||
Note: Any <command> blocks from the original task will be automatically appended to your summary wrapped in <system-reminder> tags. You do not need to include them in your summary text.
|
||||
|
||||
There may be additional summarization instructions provided in the included context. If so, remember to follow these instructions when creating the above summary. Examples of instructions include:
|
||||
<example>
|
||||
## Compact Instructions
|
||||
When summarizing the conversation focus on typescript code changes and also remember the mistakes you made and how you fixed them.
|
||||
</example>
|
||||
|
||||
<example>
|
||||
# Summary instructions
|
||||
When you are using compact - please focus on test output and code changes. Include file reads verbatim.
|
||||
</example>`,
|
||||
},
|
||||
EXPLAIN: {
|
||||
template: `Explain the following code from file path \${filePath}:\${startLine}-\${endLine}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue