@@ -133,10 +155,10 @@ export default function CookiePolicy() {
Analytics cookies
- We use PostHog analytics cookies to understand how visitors interact with our website. This
- helps us improve our services and user experience. Analytics cookies are placed only if you give
- consent through our cookie banner. The lawful basis for processing these cookies is your
- consent, which you can withdraw at any time.
+ We use PostHog and HubSpot analytics cookies to understand how visitors interact with our
+ website. This helps us improve our services, user experience, and marketing efforts. Analytics
+ cookies are placed only if you give consent through our cookie banner. The lawful basis for
+ processing these cookies is your consent, which you can withdraw at any time.
Third-party services
diff --git a/apps/web-roo-code/src/app/pricing/page.tsx b/apps/web-roo-code/src/app/pricing/page.tsx
index 487c14d087..6851b47b6a 100644
--- a/apps/web-roo-code/src/app/pricing/page.tsx
+++ b/apps/web-roo-code/src/app/pricing/page.tsx
@@ -291,11 +291,7 @@ export default function PricingPage() {
To pay for Cloud Agents running time (${PRICE_CREDITS}/hour)
To pay for AI model inference costs (
-
+
varies by model
)
diff --git a/apps/web-roo-code/src/app/slack/page.tsx b/apps/web-roo-code/src/app/slack/page.tsx
new file mode 100644
index 0000000000..c1fb39cb3e
--- /dev/null
+++ b/apps/web-roo-code/src/app/slack/page.tsx
@@ -0,0 +1,401 @@
+import {
+ ArrowRight,
+ Brain,
+ CreditCard,
+ GitBranch,
+ GraduationCap,
+ Link2,
+ MessageSquare,
+ Settings,
+ Shield,
+ Slack,
+ Users,
+ Zap,
+} from "lucide-react"
+import type { LucideIcon } from "lucide-react"
+import type { Metadata } from "next"
+
+import { AnimatedBackground } from "@/components/homepage"
+import { SlackThreadDemo } from "@/components/slack/slack-thread-demo"
+import { Button } from "@/components/ui"
+import { EXTERNAL_LINKS } from "@/lib/constants"
+import { SEO } from "@/lib/seo"
+import { ogImageUrl } from "@/lib/og"
+
+const TITLE = "Roo Code for Slack"
+const DESCRIPTION =
+ "Mention @Roomote in any channel to explain code, plan features, or ship a PR, all without leaving the conversation."
+const OG_DESCRIPTION = "Your AI Team in Slack"
+const PATH = "/slack"
+
+export const metadata: Metadata = {
+ title: TITLE,
+ description: DESCRIPTION,
+ alternates: {
+ canonical: `${SEO.url}${PATH}`,
+ },
+ openGraph: {
+ title: TITLE,
+ description: DESCRIPTION,
+ url: `${SEO.url}${PATH}`,
+ siteName: SEO.name,
+ images: [
+ {
+ url: ogImageUrl(TITLE, OG_DESCRIPTION),
+ width: 1200,
+ height: 630,
+ alt: TITLE,
+ },
+ ],
+ locale: SEO.locale,
+ type: "website",
+ },
+ twitter: {
+ card: SEO.twitterCard,
+ title: TITLE,
+ description: DESCRIPTION,
+ images: [ogImageUrl(TITLE, OG_DESCRIPTION)],
+ },
+ keywords: [
+ ...SEO.keywords,
+ "slack integration",
+ "slack bot",
+ "AI in slack",
+ "code assistant slack",
+ "@Roomote",
+ "team collaboration",
+ ],
+}
+
+// Invalidate cache when a request comes in, at most once every hour.
+export const revalidate = 3600
+
+type ValueProp = {
+ icon: LucideIcon
+ title: string
+ description: string
+}
+
+const VALUE_PROPS: ValueProp[] = [
+ {
+ icon: GitBranch,
+ title: "Discussion to PR.",
+ description:
+ "Your team discusses a feature in Slack. @Roomote turns the discussion into a plan. Then builds it. All without leaving the conversation.",
+ },
+ {
+ icon: Brain,
+ title: "Thread-aware.",
+ description:
+ '@Roomote reads the full thread before responding. Ask "Can we add caching here?" and it knows exactly what code you mean.',
+ },
+ {
+ icon: Link2,
+ title: "Chain agents.",
+ description:
+ "Start with a Planner to spec it out. Then call the Coder to build it. Multi-step workflows, one Slack thread.",
+ },
+ {
+ icon: Users,
+ title: "Open to all.",
+ description:
+ "Anyone on your team can ask @Roomote to fix bugs, build features, or investigate issues. Engineering gets looped in only when needed.",
+ },
+ {
+ icon: GraduationCap,
+ title: "Built-in learning.",
+ description: "Public channel mentions show everyone how to leverage agents. Learn by watching.",
+ },
+ {
+ icon: Shield,
+ title: "Safe by design.",
+ description: "Agents never touch main/master directly. They produce branches and PRs. You approve.",
+ },
+]
+
+type WorkflowStep = {
+ step: number
+ title: string
+ description: string
+}
+
+const WORKFLOW_STEPS: WorkflowStep[] = [
+ {
+ step: 1,
+ title: "Turn the discussion into a plan",
+ description: "Your team discusses a feature. When it gets complex, summon the Planner agent.",
+ },
+ {
+ step: 2,
+ title: "Refine the plan in the thread",
+ description:
+ "The team reviews the spec in the thread, suggests changes, asks questions. Mention @Roomote again to refine.",
+ },
+ {
+ step: 3,
+ title: "Build the plan",
+ description: "Once the plan looks good, hand it off to the Coder agent to implement.",
+ },
+ {
+ step: 4,
+ title: "Review and ship",
+ description: "The Coder creates a branch and opens a PR. The team reviews, and the feature ships.",
+ },
+]
+
+type OnboardingStep = {
+ icon: LucideIcon
+ title: string
+ description: string
+ link?: {
+ href: string
+ text: string
+ }
+}
+
+const ONBOARDING_STEPS: OnboardingStep[] = [
+ {
+ icon: CreditCard,
+ title: "1. Team Plan",
+ description: "Slack requires a Team plan.",
+ link: {
+ href: EXTERNAL_LINKS.CLOUD_APP_TEAM_TRIAL,
+ text: "Start a free trial",
+ },
+ },
+ {
+ icon: Settings,
+ title: "2. Connect",
+ description: 'Sign in to Roo Code Cloud and go to Settings. Click "Connect" next to Slack.',
+ },
+ {
+ icon: Slack,
+ title: "3. Authorize",
+ description: "Authorize the Roo Code app to access your Slack workspace.",
+ },
+ {
+ icon: MessageSquare,
+ title: "4. Add to channels",
+ description: "Add @Roomote to the channels where you want it available.",
+ },
+]
+
+export default function SlackPage(): JSX.Element {
+ return (
+ <>
+ {/* Hero Section */}
+
+
+
+
+
+
+
+ Powered by Roo Code Cloud
+
+
+ @Roomote: Your AI Team in Slack
+
+
+ Mention @Roomote in any channel to explain code, plan features, or ship a PR, all
+ without leaving the conversation.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Value Props Section */}
+
+
+
+
+
+
+
+ Why your team will love using Roo Code in Slack
+
+
+ AI agents that understand context, chain together for complex work, and keep your team in
+ control.
+
+ I've created a comprehensive implementation plan for the Roo Code Slack integration
+ marketing page at{" "}
+
+ plans/slack-marketing-page-plan.md
+
+ .
+
+
+
Plan Overview
+
+
+ •Hero + dual CTAs
+
+
+ •Value props grid
+
+
+ •“Thread to Shipped Feature” workflow
+
+
+ •Onboarding steps + CTA
+
+
+
+
+ Full document:
+ View artifact
+
+
+ Want to follow up? Just @-mention me in your response.
+
")
- expect(toolUse.params.path).toBe("src")
- expect(toolUse.partial).toBe(false)
- })
-
- it("should handle consecutive tool uses without text in between", () => {
- const message =
- "file1.tsfile2.ts"
- const result = parser(message).filter((block) => !isEmptyTextContent(block))
-
- expect(result).toHaveLength(2)
-
- const toolUse1 = result[0] as ToolUse
- expect(toolUse1.type).toBe("tool_use")
- expect(toolUse1.name).toBe("read_file")
- expect(toolUse1.params.path).toBe("file1.ts")
- expect(toolUse1.partial).toBe(false)
-
- const toolUse2 = result[1] as ToolUse
- expect(toolUse2.type).toBe("tool_use")
- expect(toolUse2.name).toBe("read_file")
- expect(toolUse2.params.path).toBe("file2.ts")
- expect(toolUse2.partial).toBe(false)
- })
-
- it("should handle whitespace in parameters", () => {
- const message = " src/file.ts "
- const result = parser(message).filter((block) => !isEmptyTextContent(block))
-
- expect(result).toHaveLength(1)
- const toolUse = result[0] as ToolUse
- expect(toolUse.type).toBe("tool_use")
- expect(toolUse.name).toBe("read_file")
- expect(toolUse.params.path).toBe("src/file.ts")
- expect(toolUse.partial).toBe(false)
- })
-
- it("should handle multi-line parameters", () => {
- const message = `file.ts
- line 1
- line 2
- line 3
- `
- const result = parser(message).filter((block) => !isEmptyTextContent(block))
-
- expect(result).toHaveLength(1)
- const toolUse = result[0] as ToolUse
- expect(toolUse.type).toBe("tool_use")
- expect(toolUse.name).toBe("write_to_file")
- expect(toolUse.params.path).toBe("file.ts")
- expect(toolUse.params.content).toContain("line 1")
- expect(toolUse.params.content).toContain("line 2")
- expect(toolUse.params.content).toContain("line 3")
- expect(toolUse.partial).toBe(false)
- })
-
- it("should handle a complex message with multiple content types", () => {
- const message = `I'll help you with that task.
-
- src/index.ts
-
- Now let's modify the file:
-
- src/index.ts
- // Updated content
- console.log("Hello world");
-
-
- Let's run the code:
-
- node src/index.ts`
-
- const result = parser(message)
-
- expect(result).toHaveLength(6)
-
- // First text block
- expect(result[0].type).toBe("text")
- expect((result[0] as TextContent).content).toBe("I'll help you with that task.")
-
- // First tool use (read_file)
- expect(result[1].type).toBe("tool_use")
- expect((result[1] as ToolUse).name).toBe("read_file")
-
- // Second text block
- expect(result[2].type).toBe("text")
- expect((result[2] as TextContent).content).toContain("Now let's modify the file:")
-
- // Second tool use (write_to_file)
- expect(result[3].type).toBe("tool_use")
- expect((result[3] as ToolUse).name).toBe("write_to_file")
-
- // Third text block
- expect(result[4].type).toBe("text")
- expect((result[4] as TextContent).content).toContain("Let's run the code:")
-
- // Third tool use (execute_command)
- expect(result[5].type).toBe("tool_use")
- expect((result[5] as ToolUse).name).toBe("execute_command")
- })
- })
- })
-})
diff --git a/src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts b/src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts
deleted file mode 100644
index a32b1173ce..0000000000
--- a/src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts
+++ /dev/null
@@ -1,111 +0,0 @@
-/* eslint-disable @typescript-eslint/no-unsafe-function-type */
-
-// node --expose-gc --import tsx src/core/assistant-message/__tests__/parseAssistantMessageBenchmark.ts
-
-import { performance } from "perf_hooks"
-import { parseAssistantMessage as parseAssistantMessageV1 } from "../parseAssistantMessage"
-import { parseAssistantMessageV2 } from "../parseAssistantMessageV2"
-
-const formatNumber = (num: number): string => {
- return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
-}
-
-const measureExecutionTime = (fn: Function, input: string, iterations: number = 1000): number => {
- for (let i = 0; i < 10; i++) {
- fn(input)
- }
-
- const start = performance.now()
-
- for (let i = 0; i < iterations; i++) {
- fn(input)
- }
-
- const end = performance.now()
- return (end - start) / iterations // Average time per iteration in ms.
-}
-
-const measureMemoryUsage = (
- fn: Function,
- input: string,
- iterations: number = 100,
-): { heapUsed: number; heapTotal: number } => {
- if (global.gc) {
- // Force garbage collection if available.
- global.gc()
- } else {
- console.warn("No garbage collection hook! Run with --expose-gc for more accurate memory measurements.")
- }
-
- const initialMemory = process.memoryUsage()
-
- for (let i = 0; i < iterations; i++) {
- fn(input)
- }
-
- const finalMemory = process.memoryUsage()
-
- return {
- heapUsed: (finalMemory.heapUsed - initialMemory.heapUsed) / iterations,
- heapTotal: (finalMemory.heapTotal - initialMemory.heapTotal) / iterations,
- }
-}
-
-const testCases = [
- {
- name: "Simple text message",
- input: "This is a simple text message without any tool uses.",
- },
- {
- name: "Message with a simple tool use",
- input: "Let's read a file: src/file.ts",
- },
- {
- name: "Message with a complex tool use (write_to_file)",
- input: "src/file.ts\nfunction example() {\n // This has XML-like content: \n return true;\n}\n",
- },
- {
- name: "Message with multiple tool uses",
- input: "First file: src/file1.ts\nSecond file: src/file2.ts\nLet's write a new file: src/file3.ts\nexport function newFunction() {\n return 'Hello world';\n}\n",
- },
- {
- name: "Large message with repeated tool uses",
- input: Array(50)
- .fill(
- 'src/file.ts\noutput.tsconsole.log("hello");',
- )
- .join("\n"),
- },
-]
-
-const runBenchmark = () => {
- const maxNameLength = testCases.reduce((max, testCase) => Math.max(max, testCase.name.length), 0)
- const namePadding = maxNameLength + 2
-
- console.log(
- `| ${"Test Case".padEnd(namePadding)} | V1 Time (ms) | V2 Time (ms) | V1/V2 Ratio | V1 Heap (bytes) | V2 Heap (bytes) |`,
- )
- console.log(
- `| ${"-".repeat(namePadding)} | ------------ | ------------ | ----------- | ---------------- | ---------------- |`,
- )
-
- for (const testCase of testCases) {
- const v1Time = measureExecutionTime(parseAssistantMessageV1, testCase.input)
- const v2Time = measureExecutionTime(parseAssistantMessageV2, testCase.input)
- const timeRatio = v1Time / v2Time
-
- const v1Memory = measureMemoryUsage(parseAssistantMessageV1, testCase.input)
- const v2Memory = measureMemoryUsage(parseAssistantMessageV2, testCase.input)
-
- console.log(
- `| ${testCase.name.padEnd(namePadding)} | ` +
- `${v1Time.toFixed(4).padStart(12)} | ` +
- `${v2Time.toFixed(4).padStart(12)} | ` +
- `${timeRatio.toFixed(2).padStart(11)} | ` +
- `${formatNumber(Math.round(v1Memory.heapUsed)).padStart(16)} | ` +
- `${formatNumber(Math.round(v2Memory.heapUsed)).padStart(16)} |`,
- )
- }
-}
-
-runBenchmark()
diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts
index e90646fd9a..690861bb56 100644
--- a/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts
+++ b/src/core/assistant-message/__tests__/presentAssistantMessage-custom-tool.spec.ts
@@ -7,6 +7,11 @@ import { presentAssistantMessage } from "../presentAssistantMessage"
vi.mock("../../task/Task")
vi.mock("../../tools/validateToolUse", () => ({
validateToolUse: vi.fn(),
+ isValidToolName: vi.fn((toolName: string) =>
+ ["read_file", "write_to_file", "ask_followup_question", "attempt_completion", "use_mcp_tool"].includes(
+ toolName,
+ ),
+ ),
}))
// Mock custom tool registry - must be done inline without external variable references
@@ -49,7 +54,6 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
didCompleteReadingStream: false,
didRejectTool: false,
didAlreadyUseTool: false,
- diffEnabled: false,
consecutiveMistakeCount: 0,
clineMessages: [],
api: {
@@ -116,39 +120,7 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
// Should record as "custom_tool", not "my_custom_tool"
expect(mockTask.recordToolUsage).toHaveBeenCalledWith("custom_tool")
- expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith(
- mockTask.taskId,
- "custom_tool",
- "native",
- )
- })
-
- it("should record custom tool usage as 'custom_tool' in XML protocol", async () => {
- mockTask.assistantMessageContent = [
- {
- type: "tool_use",
- // No ID = XML protocol
- name: "my_custom_tool",
- params: { value: "test" },
- partial: false,
- },
- ]
-
- vi.mocked(customToolRegistry.has).mockReturnValue(true)
- vi.mocked(customToolRegistry.get).mockReturnValue({
- name: "my_custom_tool",
- description: "A custom tool",
- execute: vi.fn().mockResolvedValue("Custom tool result"),
- })
-
- await presentAssistantMessage(mockTask)
-
- expect(mockTask.recordToolUsage).toHaveBeenCalledWith("custom_tool")
- expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith(
- mockTask.taskId,
- "custom_tool",
- "xml",
- )
+ expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith(mockTask.taskId, "custom_tool")
})
})
@@ -201,11 +173,7 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
// Should record as "read_file", not "custom_tool"
expect(mockTask.recordToolUsage).toHaveBeenCalledWith("read_file")
- expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith(
- mockTask.taskId,
- "read_file",
- "native",
- )
+ expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith(mockTask.taskId, "read_file")
})
it("should record MCP tool usage as 'use_mcp_tool' (not custom_tool)", async () => {
@@ -247,11 +215,7 @@ describe("presentAssistantMessage - Custom Tool Recording", () => {
// Should record as "use_mcp_tool", not "custom_tool"
expect(mockTask.recordToolUsage).toHaveBeenCalledWith("use_mcp_tool")
- expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith(
- mockTask.taskId,
- "use_mcp_tool",
- "native",
- )
+ expect(TelemetryService.instance.captureToolUsage).toHaveBeenCalledWith(mockTask.taskId, "use_mcp_tool")
})
})
diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts
index 72ee430609..7316884984 100644
--- a/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts
+++ b/src/core/assistant-message/__tests__/presentAssistantMessage-images.spec.ts
@@ -4,12 +4,16 @@ import { describe, it, expect, beforeEach, vi } from "vitest"
import { Anthropic } from "@anthropic-ai/sdk"
import { presentAssistantMessage } from "../presentAssistantMessage"
import { Task } from "../../task/Task"
-import { TOOL_PROTOCOL } from "@roo-code/types"
// Mock dependencies
vi.mock("../../task/Task")
vi.mock("../../tools/validateToolUse", () => ({
validateToolUse: vi.fn(),
+ isValidToolName: vi.fn((toolName: string) =>
+ ["read_file", "write_to_file", "ask_followup_question", "attempt_completion", "use_mcp_tool"].includes(
+ toolName,
+ ),
+ ),
}))
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
@@ -20,7 +24,7 @@ vi.mock("@roo-code/telemetry", () => ({
},
}))
-describe("presentAssistantMessage - Image Handling in Native Tool Calls", () => {
+describe("presentAssistantMessage - Image Handling in Native Tool Calling", () => {
let mockTask: any
beforeEach(() => {
@@ -37,7 +41,6 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calls", () =>
didCompleteReadingStream: false,
didRejectTool: false,
didAlreadyUseTool: false,
- diffEnabled: false,
consecutiveMistakeCount: 0,
api: {
getModel: () => ({ id: "test-model", info: {} }),
@@ -74,15 +77,16 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calls", () =>
})
})
- it("should preserve images in tool_result for native protocol", async () => {
- // Set up a tool_use block with an ID (indicates native protocol)
+ it("should preserve images in tool_result for native tool calling", async () => {
+ // Set up a tool_use block with an ID (indicates native tool calling)
const toolCallId = "tool_call_123"
mockTask.assistantMessageContent = [
{
type: "tool_use",
- id: toolCallId, // ID indicates native protocol
+ id: toolCallId, // ID indicates native tool calling
name: "ask_followup_question",
params: { question: "What do you see?" },
+ nativeArgs: { question: "What do you see?", follow_up: [] },
},
]
@@ -116,7 +120,7 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calls", () =>
expect(toolResult).toBeDefined()
expect(toolResult.tool_use_id).toBe(toolCallId)
- // For native protocol, tool_result content should be a string (text only)
+ // For native tool calling, tool_result content should be a string (text only)
expect(typeof toolResult.content).toBe("string")
expect(toolResult.content).toContain("I see a cat")
@@ -126,7 +130,7 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calls", () =>
expect(imageBlocks[0].source.data).toBe("base64ImageData")
})
- it("should convert to string when no images are present (native protocol)", async () => {
+ it("should convert to string when no images are present (native tool calling)", async () => {
// Set up a tool_use block with an ID (indicates native protocol)
const toolCallId = "tool_call_456"
mockTask.assistantMessageContent = [
@@ -135,6 +139,7 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calls", () =>
id: toolCallId,
name: "ask_followup_question",
params: { question: "What is your name?" },
+ nativeArgs: { question: "What is your name?", follow_up: [] },
},
]
@@ -157,12 +162,11 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calls", () =>
expect(typeof toolResult.content).toBe("string")
})
- it("should preserve images in content array for XML protocol (existing behavior)", async () => {
- // Set up a tool_use block WITHOUT an ID (indicates XML protocol)
+ it("should fail fast when tool_use is missing id (legacy/XML-style tool call)", async () => {
+ // tool_use without an id is treated as legacy/XML-style tool call and must be rejected.
mockTask.assistantMessageContent = [
{
type: "tool_use",
- // No ID = XML protocol
name: "ask_followup_question",
params: { question: "What do you see?" },
},
@@ -176,14 +180,13 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calls", () =>
await presentAssistantMessage(mockTask)
- // For XML protocol, content is added as separate blocks
- // Check that both text and image blocks were added
- const hasTextBlock = mockTask.userMessageContent.some((item: any) => item.type === "text")
- const hasImageBlock = mockTask.userMessageContent.some((item: any) => item.type === "image")
-
- expect(hasTextBlock).toBe(true)
- // XML protocol preserves images as separate blocks in userMessageContent
- expect(hasImageBlock).toBe(true)
+ const textBlocks = mockTask.userMessageContent.filter((item: any) => item.type === "text")
+ expect(textBlocks.length).toBeGreaterThan(0)
+ expect(textBlocks.some((b: any) => String(b.text).includes("XML tool calls are no longer supported"))).toBe(
+ true,
+ )
+ // Should not proceed to execute tool or add images as tool output.
+ expect(mockTask.userMessageContent.some((item: any) => item.type === "image")).toBe(false)
})
it("should handle empty tool result gracefully", async () => {
@@ -216,7 +219,7 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calls", () =>
})
describe("Multiple tool calls handling", () => {
- it("should send tool_result with is_error for skipped tools in native protocol when didRejectTool is true", async () => {
+ it("should send tool_result with is_error for skipped tools in native tool calling when didRejectTool is true", async () => {
// Simulate multiple tool calls with native protocol (all have IDs)
const toolCallId1 = "tool_call_001"
const toolCallId2 = "tool_call_002"
@@ -261,63 +264,15 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calls", () =>
expect(textBlocks.length).toBe(0)
})
- it("should send tool_result with is_error for skipped tools in native protocol when didAlreadyUseTool is true", async () => {
- // Simulate multiple tool calls with native protocol
- const toolCallId1 = "tool_call_003"
- const toolCallId2 = "tool_call_004"
-
+ it("should reject subsequent tool calls when a legacy/XML-style tool call is encountered", async () => {
mockTask.assistantMessageContent = [
{
type: "tool_use",
- id: toolCallId1,
name: "read_file",
params: { path: "test.txt" },
},
{
type: "tool_use",
- id: toolCallId2,
- name: "write_to_file",
- params: { path: "output.txt", content: "test" },
- },
- ]
-
- // First tool was already used
- mockTask.didAlreadyUseTool = true
-
- // Process the second tool (should be skipped)
- mockTask.currentStreamingContentIndex = 1
- await presentAssistantMessage(mockTask)
-
- // Find the tool_result for the second tool
- const toolResult = mockTask.userMessageContent.find(
- (item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId2,
- )
-
- // Verify that a tool_result block was created (not a text block)
- expect(toolResult).toBeDefined()
- expect(toolResult.tool_use_id).toBe(toolCallId2)
- expect(toolResult.is_error).toBe(true)
- expect(toolResult.content).toContain("was not executed because a tool has already been used")
-
- // Ensure no text blocks were added for this rejection
- const textBlocks = mockTask.userMessageContent.filter(
- (item: any) => item.type === "text" && item.text.includes("was not executed because"),
- )
- expect(textBlocks.length).toBe(0)
- })
-
- it("should send text blocks for skipped tools in XML protocol (no tool IDs)", async () => {
- // Simulate multiple tool calls with XML protocol (no IDs)
- mockTask.assistantMessageContent = [
- {
- type: "tool_use",
- // No ID = XML protocol
- name: "read_file",
- params: { path: "test.txt" },
- },
- {
- type: "tool_use",
- // No ID = XML protocol
name: "write_to_file",
params: { path: "output.txt", content: "test" },
},
@@ -330,18 +285,15 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calls", () =>
mockTask.currentStreamingContentIndex = 1
await presentAssistantMessage(mockTask)
- // For XML protocol, should add text block (not tool_result)
- const textBlocks = mockTask.userMessageContent.filter(
- (item: any) => item.type === "text" && item.text.includes("due to user rejecting"),
+ const textBlocks = mockTask.userMessageContent.filter((item: any) => item.type === "text")
+ expect(textBlocks.some((b: any) => String(b.text).includes("XML tool calls are no longer supported"))).toBe(
+ true,
)
- expect(textBlocks.length).toBeGreaterThan(0)
-
// Ensure no tool_result blocks were added
- const toolResults = mockTask.userMessageContent.filter((item: any) => item.type === "tool_result")
- expect(toolResults.length).toBe(0)
+ expect(mockTask.userMessageContent.some((item: any) => item.type === "tool_result")).toBe(false)
})
- it("should handle partial tool blocks when didRejectTool is true in native protocol", async () => {
+ it("should handle partial tool blocks when didRejectTool is true in native tool calling", async () => {
const toolCallId = "tool_call_005"
mockTask.assistantMessageContent = [
diff --git a/src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts b/src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts
index d4ae2764a0..15a1e2d867 100644
--- a/src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts
+++ b/src/core/assistant-message/__tests__/presentAssistantMessage-unknown-tool.spec.ts
@@ -7,6 +7,7 @@ import { presentAssistantMessage } from "../presentAssistantMessage"
vi.mock("../../task/Task")
vi.mock("../../tools/validateToolUse", () => ({
validateToolUse: vi.fn(),
+ isValidToolName: vi.fn(() => false),
}))
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
@@ -34,7 +35,6 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
didCompleteReadingStream: false,
didRejectTool: false,
didAlreadyUseTool: false,
- diffEnabled: false,
consecutiveMistakeCount: 0,
clineMessages: [],
api: {
@@ -74,12 +74,12 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
})
it("should return error for unknown tool in native protocol", async () => {
- // Set up a tool_use block with an unknown tool name and an ID (native protocol)
+ // Set up a tool_use block with an unknown tool name and an ID (native tool calling)
const toolCallId = "tool_call_unknown_123"
mockTask.assistantMessageContent = [
{
type: "tool_use",
- id: toolCallId, // ID indicates native protocol
+ id: toolCallId, // ID indicates native tool calling
name: "nonexistent_tool",
params: { some: "param" },
partial: false,
@@ -114,12 +114,11 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
expect(mockTask.say).toHaveBeenCalledWith("error", "unknownToolError")
})
- it("should return error for unknown tool in XML protocol", async () => {
- // Set up a tool_use block with an unknown tool name WITHOUT an ID (XML protocol)
+ it("should fail fast when tool_use is missing id (legacy/XML-style tool call)", async () => {
+ // tool_use without an id is treated as legacy/XML-style tool call and must be rejected.
mockTask.assistantMessageContent = [
{
type: "tool_use",
- // No ID = XML protocol
name: "fake_tool_that_does_not_exist",
params: { param1: "value1" },
partial: false,
@@ -129,16 +128,12 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
// Execute presentAssistantMessage
await presentAssistantMessage(mockTask)
- // For XML protocol, error is pushed as text blocks
+ // Should not execute tool; should surface a clear error message.
const textBlocks = mockTask.userMessageContent.filter((item: any) => item.type === "text")
-
- // There should be text blocks with error message
expect(textBlocks.length).toBeGreaterThan(0)
- const hasErrorMessage = textBlocks.some(
- (block: any) =>
- block.text?.includes("fake_tool_that_does_not_exist") && block.text?.includes("does not exist"),
+ expect(textBlocks.some((b: any) => String(b.text).includes("XML tool calls are no longer supported"))).toBe(
+ true,
)
- expect(hasErrorMessage).toBe(true)
// Verify consecutiveMistakeCount was incremented
expect(mockTask.consecutiveMistakeCount).toBe(1)
@@ -146,17 +141,17 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
// Verify recordToolError was called
expect(mockTask.recordToolError).toHaveBeenCalled()
- // Verify error message was shown to user (uses i18n key)
- expect(mockTask.say).toHaveBeenCalledWith("error", "unknownToolError")
+ // Verify error message was shown to user
+ expect(mockTask.say).toHaveBeenCalledWith("error", expect.anything())
})
- it("should handle unknown tool without freezing (native protocol)", async () => {
+ it("should handle unknown tool without freezing (native tool calling)", async () => {
// This test ensures the extension doesn't freeze when an unknown tool is called
const toolCallId = "tool_call_freeze_test"
mockTask.assistantMessageContent = [
{
type: "tool_use",
- id: toolCallId, // Native protocol
+ id: toolCallId, // Native tool calling
name: "this_tool_definitely_does_not_exist",
params: {},
partial: false,
@@ -222,32 +217,6 @@ describe("presentAssistantMessage - Unknown Tool Handling", () => {
expect(mockTask.userMessageContentReady).toBe(true)
})
- it("should still work with didAlreadyUseTool flag for unknown tool", async () => {
- const toolCallId = "tool_call_already_used_test"
- mockTask.assistantMessageContent = [
- {
- type: "tool_use",
- id: toolCallId,
- name: "unknown_tool",
- params: {},
- partial: false,
- },
- ]
-
- mockTask.didAlreadyUseTool = true
-
- await presentAssistantMessage(mockTask)
-
- // When didAlreadyUseTool is true, should send error tool_result
- const toolResult = mockTask.userMessageContent.find(
- (item: any) => item.type === "tool_result" && item.tool_use_id === toolCallId,
- )
-
- expect(toolResult).toBeDefined()
- expect(toolResult.is_error).toBe(true)
- expect(toolResult.content).toContain("was not executed because a tool has already been used")
- })
-
it("should still work with didRejectTool flag for unknown tool", async () => {
const toolCallId = "tool_call_rejected_test"
mockTask.assistantMessageContent = [
diff --git a/src/core/assistant-message/index.ts b/src/core/assistant-message/index.ts
index 72201b7722..107424fc50 100644
--- a/src/core/assistant-message/index.ts
+++ b/src/core/assistant-message/index.ts
@@ -1,2 +1,2 @@
-export { type AssistantMessageContent, parseAssistantMessage } from "./parseAssistantMessage"
+export type { AssistantMessageContent } from "./types"
export { presentAssistantMessage } from "./presentAssistantMessage"
diff --git a/src/core/assistant-message/parseAssistantMessage.ts b/src/core/assistant-message/parseAssistantMessage.ts
deleted file mode 100644
index e07b8cc3db..0000000000
--- a/src/core/assistant-message/parseAssistantMessage.ts
+++ /dev/null
@@ -1,166 +0,0 @@
-import { type ToolName, toolNames } from "@roo-code/types"
-
-import { TextContent, ToolUse, McpToolUse, ToolParamName, toolParamNames } from "../../shared/tools"
-
-export type AssistantMessageContent = TextContent | ToolUse | McpToolUse
-
-export function parseAssistantMessage(assistantMessage: string): AssistantMessageContent[] {
- let contentBlocks: AssistantMessageContent[] = []
- let currentTextContent: TextContent | undefined = undefined
- let currentTextContentStartIndex = 0
- let currentToolUse: ToolUse | undefined = undefined
- let currentToolUseStartIndex = 0
- let currentParamName: ToolParamName | undefined = undefined
- let currentParamValueStartIndex = 0
- let accumulator = ""
-
- for (let i = 0; i < assistantMessage.length; i++) {
- const char = assistantMessage[i]
- accumulator += char
-
- // There should not be a param without a tool use.
- if (currentToolUse && currentParamName) {
- const currentParamValue = accumulator.slice(currentParamValueStartIndex)
- const paramClosingTag = `${currentParamName}>`
- if (currentParamValue.endsWith(paramClosingTag)) {
- // End of param value.
- // Don't trim content parameters to preserve newlines, but strip first and last newline only
- const paramValue = currentParamValue.slice(0, -paramClosingTag.length)
- currentToolUse.params[currentParamName] =
- currentParamName === "content"
- ? paramValue.replace(/^\n/, "").replace(/\n$/, "")
- : paramValue.trim()
- currentParamName = undefined
- continue
- } else {
- // Partial param value is accumulating.
- continue
- }
- }
-
- // No currentParamName.
-
- if (currentToolUse) {
- const currentToolValue = accumulator.slice(currentToolUseStartIndex)
- const toolUseClosingTag = `${currentToolUse.name}>`
- if (currentToolValue.endsWith(toolUseClosingTag)) {
- // End of a tool use.
- currentToolUse.partial = false
- contentBlocks.push(currentToolUse)
- currentToolUse = undefined
- continue
- } else {
- const possibleParamOpeningTags = toolParamNames.map((name) => `<${name}>`)
- for (const paramOpeningTag of possibleParamOpeningTags) {
- if (accumulator.endsWith(paramOpeningTag)) {
- // Start of a new parameter.
- currentParamName = paramOpeningTag.slice(1, -1) as ToolParamName
- currentParamValueStartIndex = accumulator.length
- break
- }
- }
-
- // There's no current param, and not starting a new param.
-
- // Special case for write_to_file where file contents could
- // contain the closing tag, in which case the param would have
- // closed and we end up with the rest of the file contents here.
- // To work around this, we get the string between the starting
- // content tag and the LAST content tag.
- const contentParamName: ToolParamName = "content"
-
- if (currentToolUse.name === "write_to_file" && accumulator.endsWith(`${contentParamName}>`)) {
- const toolContent = accumulator.slice(currentToolUseStartIndex)
- const contentStartTag = `<${contentParamName}>`
- const contentEndTag = `${contentParamName}>`
- const contentStartIndex = toolContent.indexOf(contentStartTag) + contentStartTag.length
- const contentEndIndex = toolContent.lastIndexOf(contentEndTag)
-
- if (contentStartIndex !== -1 && contentEndIndex !== -1 && contentEndIndex > contentStartIndex) {
- // Don't trim content to preserve newlines, but strip first and last newline only
- currentToolUse.params[contentParamName] = toolContent
- .slice(contentStartIndex, contentEndIndex)
- .replace(/^\n/, "")
- .replace(/\n$/, "")
- }
- }
-
- // Partial tool value is accumulating.
- continue
- }
- }
-
- // No currentToolUse.
-
- let didStartToolUse = false
- const possibleToolUseOpeningTags = toolNames.map((name) => `<${name}>`)
-
- for (const toolUseOpeningTag of possibleToolUseOpeningTags) {
- if (accumulator.endsWith(toolUseOpeningTag)) {
- // Start of a new tool use.
- currentToolUse = {
- type: "tool_use",
- name: toolUseOpeningTag.slice(1, -1) as ToolName,
- params: {},
- partial: true,
- }
-
- currentToolUseStartIndex = accumulator.length
-
- // This also indicates the end of the current text content.
- if (currentTextContent) {
- currentTextContent.partial = false
-
- // Remove the partially accumulated tool use tag from the
- // end of text (()
- const toolParamOpenTags = new Map()
-
- for (const name of toolNames) {
- toolUseOpenTags.set(`<${name}>`, name)
- }
-
- for (const name of toolParamNames) {
- toolParamOpenTags.set(`<${name}>`, name)
- }
-
- const len = assistantMessage.length
-
- for (let i = 0; i < len; i++) {
- const currentCharIndex = i
-
- // Parsing a tool parameter
- if (currentToolUse && currentParamName) {
- const closeTag = `${currentParamName}>`
- // Check if the string *ending* at index `i` matches the closing tag
- if (
- currentCharIndex >= closeTag.length - 1 &&
- assistantMessage.startsWith(
- closeTag,
- currentCharIndex - closeTag.length + 1, // Start checking from potential start of tag.
- )
- ) {
- // Found the closing tag for the parameter.
- const value = assistantMessage.slice(
- currentParamValueStart, // Start after the opening tag.
- currentCharIndex - closeTag.length + 1, // End before the closing tag.
- )
- // Don't trim content parameters to preserve newlines, but strip first and last newline only
- currentToolUse.params[currentParamName] =
- currentParamName === "content" ? value.replace(/^\n/, "").replace(/\n$/, "") : value.trim()
- currentParamName = undefined // Go back to parsing tool content.
- // We don't continue loop here, need to check for tool close or other params at index i.
- } else {
- continue // Still inside param value, move to next char.
- }
- }
-
- // Parsing a tool use (but not a specific parameter).
- if (currentToolUse && !currentParamName) {
- // Ensure we are not inside a parameter already.
- // Check if starting a new parameter.
- let startedNewParam = false
-
- for (const [tag, paramName] of toolParamOpenTags.entries()) {
- if (
- currentCharIndex >= tag.length - 1 &&
- assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)
- ) {
- currentParamName = paramName
- currentParamValueStart = currentCharIndex + 1 // Value starts after the tag.
- startedNewParam = true
- break
- }
- }
-
- if (startedNewParam) {
- continue // Handled start of param, move to next char.
- }
-
- // Check if closing the current tool use.
- const toolCloseTag = `${currentToolUse.name}>`
-
- if (
- currentCharIndex >= toolCloseTag.length - 1 &&
- assistantMessage.startsWith(toolCloseTag, currentCharIndex - toolCloseTag.length + 1)
- ) {
- // End of the tool use found.
- // Special handling for content params *before* finalizing the
- // tool.
- const toolContentSlice = assistantMessage.slice(
- currentToolUseStart, // From after the tool opening tag.
- currentCharIndex - toolCloseTag.length + 1, // To before the tool closing tag.
- )
-
- // Check if content parameter needs special handling
- // (write_to_file/new_rule).
- // This check is important if the closing tag was
- // missed by the parameter parsing logic (e.g., if content is
- // empty or parsing logic prioritizes tool close).
- const contentParamName: ToolParamName = "content"
- if (
- currentToolUse.name === "write_to_file" /* || currentToolUse.name === "new_rule" */ &&
- // !(contentParamName in currentToolUse.params) && // Only if not already parsed.
- toolContentSlice.includes(`<${contentParamName}>`) // Check if tag exists.
- ) {
- const contentStartTag = `<${contentParamName}>`
- const contentEndTag = `${contentParamName}>`
- const contentStart = toolContentSlice.indexOf(contentStartTag)
-
- // Use `lastIndexOf` for robustness against nested tags.
- const contentEnd = toolContentSlice.lastIndexOf(contentEndTag)
-
- if (contentStart !== -1 && contentEnd !== -1 && contentEnd > contentStart) {
- // Don't trim content to preserve newlines, but strip first and last newline only
- const contentValue = toolContentSlice
- .slice(contentStart + contentStartTag.length, contentEnd)
- .replace(/^\n/, "")
- .replace(/\n$/, "")
- currentToolUse.params[contentParamName] = contentValue
- }
- }
-
- currentToolUse.partial = false // Mark as complete.
- contentBlocks.push(currentToolUse)
- currentToolUse = undefined // Reset state.
- currentTextContentStart = currentCharIndex + 1 // Potential text starts after this tag.
- continue // Move to next char.
- }
-
- // If not starting a param and not closing the tool, continue
- // accumulating tool content implicitly.
- continue
- }
-
- // Parsing text / looking for tool start.
- if (!currentToolUse) {
- // Check if starting a new tool use.
- let startedNewTool = false
-
- for (const [tag, toolName] of toolUseOpenTags.entries()) {
- if (
- currentCharIndex >= tag.length - 1 &&
- assistantMessage.startsWith(tag, currentCharIndex - tag.length + 1)
- ) {
- // End current text block if one was active.
- if (currentTextContent) {
- currentTextContent.content = assistantMessage
- .slice(
- currentTextContentStart, // From where text started.
- currentCharIndex - tag.length + 1, // To before the tool tag starts.
- )
- .trim()
-
- currentTextContent.partial = false // Ended because tool started.
-
- if (currentTextContent.content.length > 0) {
- contentBlocks.push(currentTextContent)
- }
-
- currentTextContent = undefined
- } else {
- // Check for any text between the last block and this tag.
- const potentialText = assistantMessage
- .slice(
- currentTextContentStart, // From where text *might* have started.
- currentCharIndex - tag.length + 1, // To before the tool tag starts.
- )
- .trim()
-
- if (potentialText.length > 0) {
- contentBlocks.push({
- type: "text",
- content: potentialText,
- partial: false,
- })
- }
- }
-
- // Start the new tool use.
- currentToolUse = {
- type: "tool_use",
- name: toolName,
- params: {},
- partial: true, // Assume partial until closing tag is found.
- }
-
- currentToolUseStart = currentCharIndex + 1 // Tool content starts after the opening tag.
- startedNewTool = true
-
- break
- }
- }
-
- if (startedNewTool) {
- continue // Handled start of tool, move to next char.
- }
-
- // If not starting a tool, it must be text content.
- if (!currentTextContent) {
- // Start a new text block if we aren't already in one.
- currentTextContentStart = currentCharIndex // Text starts at the current character.
-
- // Check if the current char is the start of potential text *immediately* after a tag.
- // This needs the previous state - simpler to let slicing handle it later.
- // Resetting start index accurately is key.
- // It should be the index *after* the last processed tag.
- // The logic managing currentTextContentStart after closing tags handles this.
- currentTextContent = {
- type: "text",
- content: "", // Will be determined by slicing at the end or when a tool starts
- partial: true,
- }
- }
- // Continue accumulating text implicitly; content is extracted later.
- }
- }
-
- // Finalize any open parameter within an open tool use.
- if (currentToolUse && currentParamName) {
- const value = assistantMessage.slice(currentParamValueStart) // From param start to end of string.
- // Don't trim content parameters to preserve newlines, but strip first and last newline only
- currentToolUse.params[currentParamName] =
- currentParamName === "content" ? value.replace(/^\n/, "").replace(/\n$/, "") : value.trim()
- // Tool use remains partial.
- }
-
- // Finalize any open tool use (which might contain the finalized partial param).
- if (currentToolUse) {
- // Tool use is partial because the loop finished before its closing tag.
- contentBlocks.push(currentToolUse)
- }
- // Finalize any trailing text content.
- // Only possible if a tool use wasn't open at the very end.
- else if (currentTextContent) {
- currentTextContent.content = assistantMessage
- .slice(currentTextContentStart) // From text start to end of string.
- .trim()
-
- // Text is partial because the loop finished.
- if (currentTextContent.content.length > 0) {
- contentBlocks.push(currentTextContent)
- }
- }
-
- return contentBlocks
-}
diff --git a/src/core/assistant-message/presentAssistantMessage.ts b/src/core/assistant-message/presentAssistantMessage.ts
index 693327a022..1d69f39cc7 100644
--- a/src/core/assistant-message/presentAssistantMessage.ts
+++ b/src/core/assistant-message/presentAssistantMessage.ts
@@ -10,17 +10,14 @@ import { t } from "../../i18n"
import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../shared/tools"
-import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
import { AskIgnoredError } from "../task/AskIgnoredError"
import { Task } from "../task/Task"
-import { fetchInstructionsTool } from "../tools/FetchInstructionsTool"
import { listFilesTool } from "../tools/ListFilesTool"
import { readFileTool } from "../tools/ReadFileTool"
-import { TOOL_PROTOCOL } from "@roo-code/types"
+import { readCommandOutputTool } from "../tools/ReadCommandOutputTool"
import { writeToFileTool } from "../tools/WriteToFileTool"
-import { applyDiffTool } from "../tools/MultiApplyDiffTool"
import { searchAndReplaceTool } from "../tools/SearchAndReplaceTool"
import { searchReplaceTool } from "../tools/SearchReplaceTool"
import { editFileTool } from "../tools/EditFileTool"
@@ -36,9 +33,10 @@ import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/Atte
import { newTaskTool } from "../tools/NewTaskTool"
import { updateTodoListTool } from "../tools/UpdateTodoListTool"
import { runSlashCommandTool } from "../tools/RunSlashCommandTool"
+import { skillTool } from "../tools/SkillTool"
import { generateImageTool } from "../tools/GenerateImageTool"
import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
-import { validateToolUse } from "../tools/validateToolUse"
+import { isValidToolName, validateToolUse } from "../tools/validateToolUse"
import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
import { formatResponse } from "../prompts/responses"
@@ -128,25 +126,9 @@ export async function presentAssistantMessage(cline: Task) {
break
}
- if (cline.didAlreadyUseTool) {
- const toolCallId = mcpBlock.id
- const errorMessage = `MCP tool [${mcpBlock.name}] was not executed because a tool has already been used in this message. Only one tool may be used per message.`
-
- if (toolCallId) {
- cline.pushToolResultToUserContent({
- type: "tool_result",
- tool_use_id: toolCallId,
- content: errorMessage,
- is_error: true,
- })
- }
- break
- }
-
// Track if we've already pushed a tool result
let hasToolResult = false
const toolCallId = mcpBlock.id
- const toolProtocol = TOOL_PROTOCOL.NATIVE // MCP tools in native mode always use native protocol
// Store approval feedback to merge into tool result (GitHub #10465)
let approvalFeedback: { text: string; images?: string[] } | undefined
@@ -174,7 +156,7 @@ export async function presentAssistantMessage(cline: Task) {
// Merge approval feedback into tool result (GitHub #10465)
if (approvalFeedback) {
- const feedbackText = formatResponse.toolApprovedWithFeedback(approvalFeedback.text, toolProtocol)
+ const feedbackText = formatResponse.toolApprovedWithFeedback(approvalFeedback.text)
resultContent = `${feedbackText}\n\n${resultContent}`
// Add feedback images to the image blocks
@@ -197,7 +179,6 @@ export async function presentAssistantMessage(cline: Task) {
}
hasToolResult = true
- cline.didAlreadyUseTool = true
}
const toolDescription = () => `[mcp_tool: ${mcpBlock.serverName}/${mcpBlock.toolName}]`
@@ -219,14 +200,9 @@ export async function presentAssistantMessage(cline: Task) {
if (response !== "yesButtonClicked") {
if (text) {
await cline.say("user_feedback", text, images)
- pushToolResult(
- formatResponse.toolResult(
- formatResponse.toolDeniedWithFeedback(text, toolProtocol),
- images,
- ),
- )
+ pushToolResult(formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images))
} else {
- pushToolResult(formatResponse.toolDenied(toolProtocol))
+ pushToolResult(formatResponse.toolDenied())
}
cline.didRejectTool = true
return false
@@ -254,12 +230,12 @@ export async function presentAssistantMessage(cline: Task) {
"error",
`Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
)
- pushToolResult(formatResponse.toolError(errorString, toolProtocol))
+ pushToolResult(formatResponse.toolError(errorString))
}
if (!mcpBlock.partial) {
cline.recordToolUsage("use_mcp_tool") // Record as use_mcp_tool for analytics
- TelemetryService.instance.captureToolUsage(cline.taskId, "use_mcp_tool", toolProtocol)
+ TelemetryService.instance.captureToolUsage(cline.taskId, "use_mcp_tool")
}
// Resolve sanitized server name back to original server name
@@ -297,8 +273,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag: (tag, text) => text || "",
- toolProtocol,
})
break
}
@@ -313,58 +287,20 @@ export async function presentAssistantMessage(cline: Task) {
// Have to do this for partial and complete since sending
// content in thinking tags to markdown renderer will
// automatically be removed.
- // Remove end substrings of (with optional line break
- // after) and (with optional line break before).
- // - Needs to be separate since we dont want to remove the line
- // break before the first tag.
- // - Needs to happen before the xml parsing below.
+ // Strip any streamed tags from text output.
content = content.replace(/\s?/g, "")
content = content.replace(/\s?<\/thinking>/g, "")
- // Remove partial XML tag at the very end of the content (for
- // tool use and thinking tags), Prevents scrollview from
- // jumping when tags are automatically removed.
- const lastOpenBracketIndex = content.lastIndexOf("<")
-
- if (lastOpenBracketIndex !== -1) {
- const possibleTag = content.slice(lastOpenBracketIndex)
-
- // Check if there's a '>' after the last '<' (i.e., if the
- // tag is complete) (complete thinking and tool tags will
- // have been removed by now.)
- const hasCloseBracket = possibleTag.includes(">")
-
- if (!hasCloseBracket) {
- // Extract the potential tag name.
- let tagContent: string
-
- if (possibleTag.startsWith("")) {
- tagContent = possibleTag.slice(2).trim()
- } else {
- tagContent = possibleTag.slice(1).trim()
- }
-
- // Check if tagContent is likely an incomplete tag name
- // (letters and underscores only).
- const isLikelyTagName = /^[a-zA-Z_]+$/.test(tagContent)
-
- // Preemptively remove < or to keep from these
- // artifacts showing up in chat (also handles closing
- // thinking tags).
- const isOpeningOrClosing = possibleTag === "<" || possibleTag === ""
-
- // If the tag is incomplete and at the end, remove it
- // from the content.
- if (isOpeningOrClosing || isLikelyTagName) {
- content = content.slice(0, lastOpenBracketIndex).trim()
- }
- }
+ // Tool calling is native-only. If the model emits XML-style tool tags in a text block,
+ // fail fast with a clear error.
+ if (containsXmlToolMarkup(content)) {
+ const errorMessage =
+ "XML tool calls are no longer supported. Remove any XML tool markup (e.g. ...) and use native tool calling instead."
+ cline.consecutiveMistakeCount++
+ await cline.say("error", errorMessage)
+ cline.userMessageContent.push({ type: "text", text: errorMessage })
+ cline.didAlreadyUseTool = true
+ break
}
}
@@ -372,6 +308,30 @@ export async function presentAssistantMessage(cline: Task) {
break
}
case "tool_use": {
+ // Native tool calling is the only supported tool calling mechanism.
+ // A tool_use block without an id is invalid and cannot be executed.
+ const toolCallId = (block as any).id as string | undefined
+ if (!toolCallId) {
+ const errorMessage =
+ "Invalid tool call: missing tool_use.id. XML tool calls are no longer supported. Remove any XML tool markup (e.g. ...) and use native tool calling instead."
+ // Record a tool error for visibility/telemetry. Use the reported tool name if present.
+ try {
+ if (
+ typeof (cline as any).recordToolError === "function" &&
+ typeof (block as any).name === "string"
+ ) {
+ ;(cline as any).recordToolError((block as any).name as ToolName, errorMessage)
+ }
+ } catch {
+ // Best-effort only
+ }
+ cline.consecutiveMistakeCount++
+ await cline.say("error", errorMessage)
+ cline.userMessageContent.push({ type: "text", text: errorMessage })
+ cline.didAlreadyUseTool = true
+ break
+ }
+
// Fetch state early so it's available for toolDescription and validation
const state = await cline.providerRef.deref()?.getState()
const { mode, customModes, experiments: stateExperiments } = state ?? {}
@@ -387,29 +347,11 @@ export async function presentAssistantMessage(cline: Task) {
return readFileTool.getReadFileToolDescription(block.name, block.nativeArgs)
}
return readFileTool.getReadFileToolDescription(block.name, block.params)
- case "fetch_instructions":
- return `[${block.name} for '${block.params.task}']`
case "write_to_file":
return `[${block.name} for '${block.params.path}']`
case "apply_diff":
- // Handle both legacy format and new multi-file format
- if (block.params.path) {
- return `[${block.name} for '${block.params.path}']`
- } else if (block.params.args) {
- // Try to extract first file path from args for display
- const match = block.params.args.match(/.*?([^<]+)<\/path>/s)
- if (match) {
- const firstPath = match[1]
- // Check if there are multiple files
- const fileCount = (block.params.args.match(//g) || []).length
- if (fileCount > 1) {
- return `[${block.name} for '${firstPath}' and ${fileCount - 1} more file${fileCount > 2 ? "s" : ""}]`
- } else {
- return `[${block.name} for '${firstPath}']`
- }
- }
- }
- return `[${block.name}]`
+ // Native-only: tool args are structured (no XML payloads).
+ return block.params?.path ? `[${block.name} for '${block.params.path}']` : `[${block.name}]`
case "search_files":
return `[${block.name} for '${block.params.regex}'${
block.params.file_pattern ? ` in '${block.params.file_pattern}'` : ""
@@ -436,8 +378,10 @@ export async function presentAssistantMessage(cline: Task) {
return `[${block.name}]`
case "switch_mode":
return `[${block.name} to '${block.params.mode_slug}'${block.params.reason ? ` because: ${block.params.reason}` : ""}]`
- case "codebase_search": // Add case for the new tool
+ case "codebase_search":
return `[${block.name} for '${block.params.query}']`
+ case "read_command_output":
+ return `[${block.name} for '${block.params.artifact_id}']`
case "update_todo_list":
return `[${block.name}]`
case "new_task": {
@@ -448,6 +392,8 @@ export async function presentAssistantMessage(cline: Task) {
}
case "run_slash_command":
return `[${block.name} for '${block.params.command}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
+ case "skill":
+ return `[${block.name} for '${block.params.skill}'${block.params.args ? ` with args: ${block.params.args}` : ""}]`
case "generate_image":
return `[${block.name} for '${block.params.path}']`
default:
@@ -457,185 +403,105 @@ export async function presentAssistantMessage(cline: Task) {
if (cline.didRejectTool) {
// Ignore any tool content after user has rejected tool once.
- // For native protocol, we must send a tool_result for every tool_use to avoid API errors
- const toolCallId = block.id
+ // For native tool calling, we must send a tool_result for every tool_use to avoid API errors
const errorMessage = !block.partial
? `Skipping tool ${toolDescription()} due to user rejecting a previous tool.`
: `Tool ${toolDescription()} was interrupted and not executed due to user rejecting a previous tool.`
- if (toolCallId) {
- // Native protocol: MUST send tool_result for every tool_use
- cline.pushToolResultToUserContent({
- type: "tool_result",
- tool_use_id: toolCallId,
- content: errorMessage,
- is_error: true,
- })
- } else {
- // XML protocol: send as text
- cline.userMessageContent.push({
- type: "text",
- text: errorMessage,
- })
- }
+ cline.pushToolResultToUserContent({
+ type: "tool_result",
+ tool_use_id: toolCallId,
+ content: errorMessage,
+ is_error: true,
+ })
break
}
- if (cline.didAlreadyUseTool) {
- // Ignore any content after a tool has already been used.
- // For native protocol, we must send a tool_result for every tool_use to avoid API errors
- const toolCallId = block.id
- const errorMessage = `Tool [${block.name}] was not executed because a tool has already been used in this message. Only one tool may be used per message. You must assess the first tool's result before proceeding to use the next tool.`
-
- if (toolCallId) {
- // Native protocol: MUST send tool_result for every tool_use
- cline.pushToolResultToUserContent({
- type: "tool_result",
- tool_use_id: toolCallId,
- content: errorMessage,
- is_error: true,
- })
- } else {
- // XML protocol: send as text
- cline.userMessageContent.push({
- type: "text",
- text: errorMessage,
- })
- }
-
- break
- }
-
- // Track if we've already pushed a tool result for this tool call (native protocol only)
+ // Track if we've already pushed a tool result for this tool call (native tool calling only)
let hasToolResult = false
- // Determine protocol by checking if this tool call has an ID.
- // Native protocol tool calls ALWAYS have an ID (set when parsed from tool_call chunks).
- // XML protocol tool calls NEVER have an ID (parsed from XML text).
- const toolCallId = (block as any).id
- const toolProtocol = toolCallId ? TOOL_PROTOCOL.NATIVE : TOOL_PROTOCOL.XML
+ // If this is a native tool call but the parser couldn't construct nativeArgs
+ // (e.g., malformed/unfinished JSON in a streaming tool call), we must NOT attempt to
+ // execute the tool. Instead, emit exactly one structured tool_result so the provider
+ // receives a matching tool_result for the tool_use_id.
+ //
+ // This avoids executing an invalid tool_use block and prevents duplicate/fragmented
+ // error reporting.
+ if (!block.partial) {
+ const customTool = stateExperiments?.customTools ? customToolRegistry.get(block.name) : undefined
+ const isKnownTool = isValidToolName(String(block.name), stateExperiments)
+ if (isKnownTool && !block.nativeArgs && !customTool) {
+ const errorMessage =
+ `Invalid tool call for '${block.name}': missing nativeArgs. ` +
+ `This usually means the model streamed invalid or incomplete arguments and the call could not be finalized.`
- // Multiple native tool calls feature is on hold - always disabled
- // Previously resolved from experiments.isEnabled(..., EXPERIMENT_IDS.MULTIPLE_NATIVE_TOOL_CALLS)
- const isMultipleNativeToolCallsEnabled = false
+ cline.consecutiveMistakeCount++
+ try {
+ cline.recordToolError(block.name as ToolName, errorMessage)
+ } catch {
+ // Best-effort only
+ }
+
+ // Push tool_result directly without setting didAlreadyUseTool so streaming can
+ // continue gracefully.
+ cline.pushToolResultToUserContent({
+ type: "tool_result",
+ tool_use_id: toolCallId,
+ content: formatResponse.toolError(errorMessage),
+ is_error: true,
+ })
+
+ break
+ }
+ }
// Store approval feedback to merge into tool result (GitHub #10465)
let approvalFeedback: { text: string; images?: string[] } | undefined
const pushToolResult = (content: ToolResponse) => {
- if (toolProtocol === TOOL_PROTOCOL.NATIVE) {
- // For native protocol, only allow ONE tool_result per tool call
- if (hasToolResult) {
- console.warn(
- `[presentAssistantMessage] Skipping duplicate tool_result for tool_use_id: ${toolCallId}`,
- )
- return
- }
+ // Native tool calling: only allow ONE tool_result per tool call
+ if (hasToolResult) {
+ console.warn(
+ `[presentAssistantMessage] Skipping duplicate tool_result for tool_use_id: ${toolCallId}`,
+ )
+ return
+ }
- // For native protocol, tool_result content must be a string
- // Images are added as separate blocks in the user message
- let resultContent: string
- let imageBlocks: Anthropic.ImageBlockParam[] = []
+ let resultContent: string
+ let imageBlocks: Anthropic.ImageBlockParam[] = []
- if (typeof content === "string") {
- resultContent = content || "(tool did not return anything)"
- } else {
- // Separate text and image blocks
- const textBlocks = content.filter((item) => item.type === "text")
- imageBlocks = content.filter((item) => item.type === "image") as Anthropic.ImageBlockParam[]
-
- // Convert text blocks to string for tool_result
- resultContent =
- textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") ||
- "(tool did not return anything)"
- }
-
- // Merge approval feedback into tool result (GitHub #10465)
- if (approvalFeedback) {
- const feedbackText = formatResponse.toolApprovedWithFeedback(
- approvalFeedback.text,
- toolProtocol,
- )
- resultContent = `${feedbackText}\n\n${resultContent}`
-
- // Add feedback images to the image blocks
- if (approvalFeedback.images) {
- const feedbackImageBlocks = formatResponse.imageBlocks(approvalFeedback.images)
- imageBlocks = [...feedbackImageBlocks, ...imageBlocks]
- }
- }
-
- // Add tool_result with text content only
- cline.pushToolResultToUserContent({
- type: "tool_result",
- tool_use_id: toolCallId,
- content: resultContent,
- })
-
- // Add image blocks separately after tool_result
- if (imageBlocks.length > 0) {
- cline.userMessageContent.push(...imageBlocks)
- }
-
- hasToolResult = true
+ if (typeof content === "string") {
+ resultContent = content || "(tool did not return anything)"
} else {
- // For XML protocol, add as text blocks (legacy behavior)
- let resultContent: string
+ const textBlocks = content.filter((item) => item.type === "text")
+ imageBlocks = content.filter((item) => item.type === "image") as Anthropic.ImageBlockParam[]
+ resultContent =
+ textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") ||
+ "(tool did not return anything)"
+ }
- if (typeof content === "string") {
- resultContent = content || "(tool did not return anything)"
- } else {
- const textBlocks = content.filter((item) => item.type === "text")
- resultContent =
- textBlocks.map((item) => (item as Anthropic.TextBlockParam).text).join("\n") ||
- "(tool did not return anything)"
- }
-
- // Merge approval feedback into tool result (GitHub #10465)
- if (approvalFeedback) {
- const feedbackText = formatResponse.toolApprovedWithFeedback(
- approvalFeedback.text,
- toolProtocol,
- )
- resultContent = `${feedbackText}\n\n${resultContent}`
- }
-
- cline.userMessageContent.push({ type: "text", text: `${toolDescription()} Result:` })
-
- if (typeof content === "string") {
- cline.userMessageContent.push({
- type: "text",
- text: resultContent,
- })
- } else {
- // Add text content with merged feedback
- cline.userMessageContent.push({
- type: "text",
- text: resultContent,
- })
- // Add any images from the tool result
- const imageBlocks = content.filter((item) => item.type === "image")
- if (imageBlocks.length > 0) {
- cline.userMessageContent.push(...imageBlocks)
- }
+ // Merge approval feedback into tool result (GitHub #10465)
+ if (approvalFeedback) {
+ const feedbackText = formatResponse.toolApprovedWithFeedback(approvalFeedback.text)
+ resultContent = `${feedbackText}\n\n${resultContent}`
+ if (approvalFeedback.images) {
+ const feedbackImageBlocks = formatResponse.imageBlocks(approvalFeedback.images)
+ imageBlocks = [...feedbackImageBlocks, ...imageBlocks]
}
}
- // For XML protocol: Only one tool per message is allowed
- // For native protocol with experimental flag enabled: Multiple tools can be executed in sequence
- // For native protocol with experimental flag disabled: Single tool per message (default safe behavior)
- if (toolProtocol === TOOL_PROTOCOL.XML) {
- // Once a tool result has been collected, ignore all other tool
- // uses since we should only ever present one tool result per
- // message (XML protocol only).
- cline.didAlreadyUseTool = true
- } else if (toolProtocol === TOOL_PROTOCOL.NATIVE && !isMultipleNativeToolCallsEnabled) {
- // For native protocol with experimental flag disabled, enforce single tool per message
- cline.didAlreadyUseTool = true
+ cline.pushToolResultToUserContent({
+ type: "tool_result",
+ tool_use_id: toolCallId,
+ content: resultContent,
+ })
+
+ if (imageBlocks.length > 0) {
+ cline.userMessageContent.push(...imageBlocks)
}
- // If toolProtocol is NATIVE and isMultipleNativeToolCallsEnabled is true,
- // allow multiple tool calls in sequence (don't set didAlreadyUseTool)
+
+ hasToolResult = true
}
const askApproval = async (
@@ -656,14 +522,9 @@ export async function presentAssistantMessage(cline: Task) {
// Handle both messageResponse and noButtonClicked with text.
if (text) {
await cline.say("user_feedback", text, images)
- pushToolResult(
- formatResponse.toolResult(
- formatResponse.toolDeniedWithFeedback(text, toolProtocol),
- images,
- ),
- )
+ pushToolResult(formatResponse.toolResult(formatResponse.toolDeniedWithFeedback(text), images))
} else {
- pushToolResult(formatResponse.toolDenied(toolProtocol))
+ pushToolResult(formatResponse.toolDenied())
}
cline.didRejectTool = true
return false
@@ -702,34 +563,7 @@ export async function presentAssistantMessage(cline: Task) {
`Error ${action}:\n${error.message ?? JSON.stringify(serializeError(error), null, 2)}`,
)
- pushToolResult(formatResponse.toolError(errorString, toolProtocol))
- }
-
- // If block is partial, remove partial closing tag so its not
- // presented to user.
- const removeClosingTag = (tag: ToolParamName, text?: string): string => {
- if (!block.partial) {
- return text || ""
- }
-
- if (!text) {
- return ""
- }
-
- // This regex dynamically constructs a pattern to match the
- // closing tag:
- // - Optionally matches whitespace before the tag.
- // - Matches '<' or '' optionally followed by any subset of
- // characters from the tag name.
- const tagRegex = new RegExp(
- `\\s?<\/?${tag
- .split("")
- .map((char) => `(?:${char})?`)
- .join("")}$`,
- "g",
- )
-
- return text.replace(tagRegex, "")
+ pushToolResult(formatResponse.toolError(errorString))
}
// Keep browser open during an active session so other tools can run.
@@ -765,7 +599,7 @@ export async function presentAssistantMessage(cline: Task) {
const isCustomTool = stateExperiments?.customTools && customToolRegistry.has(block.name)
const recordName = isCustomTool ? "custom_tool" : block.name
cline.recordToolUsage(recordName)
- TelemetryService.instance.captureToolUsage(cline.taskId, recordName, toolProtocol)
+ TelemetryService.instance.captureToolUsage(cline.taskId, recordName)
}
// Validate tool use before execution - ONLY for complete (non-partial) blocks.
@@ -785,7 +619,7 @@ export async function presentAssistantMessage(cline: Task) {
block.name as ToolName,
mode ?? defaultModeSlug,
customModes ?? [],
- { apply_diff: cline.diffEnabled },
+ {},
block.params,
stateExperiments,
includedTools,
@@ -793,24 +627,18 @@ export async function presentAssistantMessage(cline: Task) {
} catch (error) {
cline.consecutiveMistakeCount++
// For validation errors (unknown tool, tool not allowed for mode), we need to:
- // 1. Send a tool_result with the error (required for native protocol)
+ // 1. Send a tool_result with the error (required for native tool calling)
// 2. NOT set didAlreadyUseTool = true (the tool was never executed, just failed validation)
// This prevents the stream from being interrupted with "Response interrupted by tool use result"
// which would cause the extension to appear to hang
- const errorContent = formatResponse.toolError(error.message, toolProtocol)
-
- if (toolProtocol === TOOL_PROTOCOL.NATIVE && toolCallId) {
- // For native protocol, push tool_result directly without setting didAlreadyUseTool
- cline.pushToolResultToUserContent({
- type: "tool_result",
- tool_use_id: toolCallId,
- content: typeof errorContent === "string" ? errorContent : "(validation error)",
- is_error: true,
- })
- } else {
- // For XML protocol, use the standard pushToolResult
- pushToolResult(errorContent)
- }
+ const errorContent = formatResponse.toolError(error.message)
+ // Push tool_result directly without setting didAlreadyUseTool
+ cline.pushToolResultToUserContent({
+ type: "tool_result",
+ tool_use_id: toolCallId,
+ content: typeof errorContent === "string" ? errorContent : "(validation error)",
+ is_error: true,
+ })
break
}
@@ -862,7 +690,6 @@ export async function presentAssistantMessage(cline: Task) {
pushToolResult(
formatResponse.toolError(
`Tool call repetition limit reached for ${block.name}. Please try a different approach.`,
- toolProtocol,
),
)
break
@@ -876,8 +703,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "update_todo_list":
@@ -885,59 +710,22 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
- case "apply_diff": {
+ case "apply_diff":
await checkpointSaveAndMark(cline)
-
- // Check if this tool call came from native protocol by checking for ID
- // Native calls always have IDs, XML calls never do
- if (toolProtocol === TOOL_PROTOCOL.NATIVE) {
- await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- }
-
- // Get the provider and state to check experiment settings
- const provider = cline.providerRef.deref()
- let isMultiFileApplyDiffEnabled = false
-
- if (provider) {
- const state = await provider.getState()
- isMultiFileApplyDiffEnabled = experiments.isEnabled(
- state.experiments ?? {},
- EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF,
- )
- }
-
- if (isMultiFileApplyDiffEnabled) {
- await applyDiffTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
- } else {
- await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- }
+ await applyDiffToolClass.handle(cline, block as ToolUse<"apply_diff">, {
+ askApproval,
+ handleError,
+ pushToolResult,
+ })
break
- }
case "search_and_replace":
await checkpointSaveAndMark(cline)
await searchAndReplaceTool.handle(cline, block as ToolUse<"search_and_replace">, {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "search_replace":
@@ -946,8 +734,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "edit_file":
@@ -956,8 +742,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "apply_patch":
@@ -966,8 +750,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "read_file":
@@ -976,17 +758,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
- })
- break
- case "fetch_instructions":
- await fetchInstructionsTool.handle(cline, block as ToolUse<"fetch_instructions">, {
- askApproval,
- handleError,
- pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "list_files":
@@ -994,8 +765,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "codebase_search":
@@ -1003,8 +772,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "search_files":
@@ -1012,8 +779,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "browser_action":
@@ -1023,7 +788,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
)
break
case "execute_command":
@@ -1031,8 +795,13 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
+ })
+ break
+ case "read_command_output":
+ await readCommandOutputTool.handle(cline, block as ToolUse<"read_command_output">, {
+ askApproval,
+ handleError,
+ pushToolResult,
})
break
case "use_mcp_tool":
@@ -1040,8 +809,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "access_mcp_resource":
@@ -1049,8 +816,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "ask_followup_question":
@@ -1058,8 +823,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "switch_mode":
@@ -1067,17 +830,14 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "new_task":
+ await checkpointSaveAndMark(cline)
await newTaskTool.handle(cline, block as ToolUse<"new_task">, {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
toolCallId: block.id,
})
break
@@ -1086,10 +846,8 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
askFinishSubTaskApproval,
toolDescription,
- toolProtocol,
}
await attemptCompletionTool.handle(
cline,
@@ -1103,8 +861,13 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
+ })
+ break
+ case "skill":
+ await skillTool.handle(cline, block as ToolUse<"skill">, {
+ askApproval,
+ handleError,
+ pushToolResult,
})
break
case "generate_image":
@@ -1113,13 +876,11 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
default: {
// Handle unknown/invalid tool names OR custom tools
- // This is critical for native protocol where every tool_use MUST have a tool_result
+ // This is critical for native tool calling where every tool_use MUST have a tool_result
// CRITICAL: Don't process partial blocks for unknown tools - just let them stream in.
// If we try to show errors for partial blocks, we'd show the error on every streaming chunk,
@@ -1142,7 +903,7 @@ export async function presentAssistantMessage(cline: Task) {
console.error(message)
cline.consecutiveMistakeCount++
await cline.say("error", message)
- pushToolResult(formatResponse.toolError(message, toolProtocol))
+ pushToolResult(formatResponse.toolError(message))
break
}
}
@@ -1173,18 +934,14 @@ export async function presentAssistantMessage(cline: Task) {
cline.consecutiveMistakeCount++
cline.recordToolError(block.name as ToolName, errorMessage)
await cline.say("error", t("tools:unknownToolError", { toolName: block.name }))
- // Push tool_result directly for native protocol WITHOUT setting didAlreadyUseTool
+ // Push tool_result directly WITHOUT setting didAlreadyUseTool
// This prevents the stream from being interrupted with "Response interrupted by tool use result"
- if (toolProtocol === TOOL_PROTOCOL.NATIVE && toolCallId) {
- cline.pushToolResultToUserContent({
- type: "tool_result",
- tool_use_id: toolCallId,
- content: formatResponse.toolError(errorMessage, toolProtocol),
- is_error: true,
- })
- } else {
- pushToolResult(formatResponse.toolError(errorMessage, toolProtocol))
- }
+ cline.pushToolResultToUserContent({
+ type: "tool_result",
+ tool_use_id: toolCallId,
+ content: formatResponse.toolError(errorMessage),
+ is_error: true,
+ })
break
}
}
@@ -1264,3 +1021,47 @@ async function checkpointSaveAndMark(task: Task) {
console.error(`[Task#presentAssistantMessage] Error saving checkpoint: ${error.message}`, error)
}
}
+
+function containsXmlToolMarkup(text: string): boolean {
+ // Keep this intentionally narrow: only reject XML-style tool tags matching our tool names.
+ // Avoid regex so we don't keep legacy XML parsing artifacts around.
+ // Note: This is a best-effort safeguard; tool_use blocks without an id are rejected elsewhere.
+
+ // First, strip out content inside markdown code fences to avoid false positives
+ // when users paste documentation or examples containing tool tag references.
+ // This handles both fenced code blocks (```) and inline code (`).
+ const textWithoutCodeBlocks = text
+ .replace(/```[\s\S]*?```/g, "") // Remove fenced code blocks
+ .replace(/`[^`]+`/g, "") // Remove inline code
+
+ const lower = textWithoutCodeBlocks.toLowerCase()
+ if (!lower.includes("<") || !lower.includes(">")) {
+ return false
+ }
+
+ const toolNames = [
+ "access_mcp_resource",
+ "apply_diff",
+ "apply_patch",
+ "ask_followup_question",
+ "attempt_completion",
+ "browser_action",
+ "codebase_search",
+ "edit_file",
+ "execute_command",
+ "generate_image",
+ "list_files",
+ "new_task",
+ "read_command_output",
+ "read_file",
+ "search_and_replace",
+ "search_files",
+ "search_replace",
+ "switch_mode",
+ "update_todo_list",
+ "use_mcp_tool",
+ "write_to_file",
+ ] as const
+
+ return toolNames.some((name) => lower.includes(`<${name}`) || lower.includes(`${name}`))
+}
diff --git a/src/core/assistant-message/types.ts b/src/core/assistant-message/types.ts
new file mode 100644
index 0000000000..7cd890cfdd
--- /dev/null
+++ b/src/core/assistant-message/types.ts
@@ -0,0 +1,3 @@
+import type { TextContent, ToolUse, McpToolUse } from "../../shared/tools"
+
+export type AssistantMessageContent = TextContent | ToolUse | McpToolUse
diff --git a/src/core/auto-approval/index.ts b/src/core/auto-approval/index.ts
index f295140501..f9de2ccfe3 100644
--- a/src/core/auto-approval/index.ts
+++ b/src/core/auto-approval/index.ts
@@ -151,14 +151,11 @@ export async function checkAutoApproval({
return { decision: "approve" }
}
- if (tool?.tool === "fetchInstructions") {
- if (tool.content === "create_mode") {
- return state.alwaysAllowModeSwitch === true ? { decision: "approve" } : { decision: "ask" }
- }
-
- if (tool.content === "create_mcp_server") {
- return state.alwaysAllowMcp === true ? { decision: "approve" } : { decision: "ask" }
- }
+ // The skill tool only loads pre-defined instructions from built-in, global, or project skills.
+ // It does not read arbitrary files - skills must be explicitly installed/defined by the user.
+ // Auto-approval is intentional to provide a seamless experience when loading task instructions.
+ if (tool.tool === "skill") {
+ return { decision: "approve" }
}
if (tool?.tool === "switchMode") {
diff --git a/src/core/condense/__tests__/condense.spec.ts b/src/core/condense/__tests__/condense.spec.ts
index bea7d50ac1..c209fa9724 100644
--- a/src/core/condense/__tests__/condense.spec.ts
+++ b/src/core/condense/__tests__/condense.spec.ts
@@ -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 /prr #123 more text',
+ }
+
+ const result = extractCommandBlocks(message)
+ expect(result).toBe('/prr #123')
+ })
+
+ it("should extract multiple command blocks", () => {
+ const message: ApiMessage = {
+ role: "user",
+ content: '/prr #123 text /mode code',
+ }
+
+ const result = extractCommandBlocks(message)
+ expect(result).toBe('/prr #123\n/mode code')
+ })
+
+ it("should extract command blocks from array content", () => {
+ const message: ApiMessage = {
+ role: "user",
+ content: [
+ { type: "text", text: "Some user text" },
+ { type: "text", text: 'Help content' },
+ ],
+ }
+
+ const result = extractCommandBlocks(message)
+ expect(result).toBe('Help content')
+ })
+
+ 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: `
+Line 1
+Line 2
+`,
+ }
+
+ 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" },
@@ -77,59 +136,95 @@ describe("Condense", () => {
{ role: "user", content: "Ninth message" },
]
- const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
+ const result = await summarizeConversation({
+ messages,
+ apiHandler: mockApiHandler,
+ systemPrompt: "System prompt",
+ taskId,
+ isAutomaticTrigger: 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)
+ const result = await summarizeConversation({
+ messages,
+ apiHandler: mockApiHandler,
+ systemPrompt: "System prompt",
+ taskId,
+ isAutomaticTrigger: 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 blocks in the summary", async () => {
+ const messages: ApiMessage[] = [
+ {
+ role: "user",
+ content: [
+ { type: "text", text: "Some user text" },
+ { type: "text", text: 'Help content' },
+ ],
+ },
+ { 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,
+ apiHandler: mockApiHandler,
+ systemPrompt: "System prompt",
+ taskId,
+ isAutomaticTrigger: 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: "..." with command blocks
+ expect(contentArray).toHaveLength(2)
+ expect(contentArray[0].text).toContain("## Conversation Summary")
+ expect(contentArray[1].text).toContain('')
+ expect(contentArray[1].text).toContain("")
+ expect(contentArray[1].text).toContain("Active Workflows")
})
it("should handle complex first message content", async () => {
@@ -150,43 +245,53 @@ describe("Condense", () => {
{ role: "user", content: "Perfect!" },
]
- const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
+ const result = await summarizeConversation({
+ messages,
+ apiHandler: mockApiHandler,
+ systemPrompt: "System prompt",
+ taskId,
+ isAutomaticTrigger: 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)
+ const result = await summarizeConversation({
+ messages,
+ apiHandler: mockApiHandler,
+ systemPrompt: "System prompt",
+ taskId,
+ isAutomaticTrigger: 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: "user", content: "Previous summary", isSummary: true },
]
- const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
+ const result = await summarizeConversation({
+ messages,
+ apiHandler: mockApiHandler,
+ systemPrompt: "System prompt",
+ taskId,
+ isAutomaticTrigger: 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("")
@@ -217,7 +322,13 @@ describe("Condense", () => {
{ role: "user", content: "Seventh" },
]
- const result = await summarizeConversation(messages, emptyHandler, "System prompt", taskId, 5000, false)
+ const result = await summarizeConversation({
+ messages,
+ apiHandler: emptyHandler,
+ systemPrompt: "System prompt",
+ taskId,
+ isAutomaticTrigger: false,
+ })
expect(result.error).toBeDefined()
expect(result.messages).toEqual(messages)
@@ -225,6 +336,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 +427,33 @@ 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])
})
})
})
diff --git a/src/core/condense/__tests__/foldedFileContext.spec.ts b/src/core/condense/__tests__/foldedFileContext.spec.ts
new file mode 100644
index 0000000000..3bd9b390f5
--- /dev/null
+++ b/src/core/condense/__tests__/foldedFileContext.spec.ts
@@ -0,0 +1,391 @@
+// npx vitest src/core/condense/__tests__/foldedFileContext.spec.ts
+
+import * as path from "path"
+import { Anthropic } from "@anthropic-ai/sdk"
+import type { ModelInfo } from "@roo-code/types"
+import { TelemetryService } from "@roo-code/telemetry"
+import { BaseProvider } from "../../../api/providers/base-provider"
+
+// Mock the tree-sitter module
+vi.mock("../../../services/tree-sitter", () => ({
+ parseSourceCodeDefinitionsForFile: vi.fn(),
+}))
+
+// Mock generateFoldedFileContext for summarizeConversation tests
+vi.mock("../foldedFileContext", async (importOriginal) => {
+ const actual = await importOriginal()
+ return {
+ ...actual,
+ generateFoldedFileContext: vi.fn().mockImplementation(actual.generateFoldedFileContext),
+ }
+})
+
+import { generateFoldedFileContext } from "../foldedFileContext"
+import { parseSourceCodeDefinitionsForFile } from "../../../services/tree-sitter"
+
+const mockedGenerateFoldedFileContext = vi.mocked(generateFoldedFileContext)
+
+const mockedParseSourceCodeDefinitions = vi.mocked(parseSourceCodeDefinitionsForFile)
+
+describe("foldedFileContext", () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ describe("generateFoldedFileContext", () => {
+ it("should return empty content for empty file list", async () => {
+ const result = await generateFoldedFileContext([], { cwd: "/test" })
+
+ expect(result.content).toBe("")
+ expect(result.sections).toEqual([])
+ expect(result.filesProcessed).toBe(0)
+ expect(result.filesSkipped).toBe(0)
+ expect(result.characterCount).toBe(0)
+ })
+
+ it("should generate folded context for a TypeScript file with its own system-reminder block", async () => {
+ const mockDefinitions = `1--5 | export interface User
+7--12 | export function createUser(name: string): User
+14--28 | export class UserService`
+
+ mockedParseSourceCodeDefinitions.mockResolvedValue(mockDefinitions)
+
+ const result = await generateFoldedFileContext(["/test/user.ts"], { cwd: "/test" })
+
+ // Each file should be wrapped in its own block
+ expect(result.content).toContain("")
+ expect(result.content).toContain("")
+ expect(result.content).toContain("## File Context: /test/user.ts")
+ expect(result.content).toContain("interface User")
+ expect(result.content).toContain("function createUser")
+ expect(result.content).toContain("class UserService")
+ expect(result.filesProcessed).toBe(1)
+ expect(result.filesSkipped).toBe(0)
+ })
+
+ it("should generate folded context for a JavaScript file with its own system-reminder block", async () => {
+ const mockDefinitions = `1--3 | function greet(name)
+5--15 | class Calculator`
+
+ mockedParseSourceCodeDefinitions.mockResolvedValue(mockDefinitions)
+
+ const result = await generateFoldedFileContext(["/test/utils.js"], { cwd: "/test" })
+
+ expect(result.content).toContain("")
+ expect(result.content).toContain("## File Context: /test/utils.js")
+ expect(result.content).toContain("function greet")
+ expect(result.content).toContain("class Calculator")
+ expect(result.filesProcessed).toBe(1)
+ })
+
+ it("should skip files when parseSourceCodeDefinitions returns undefined", async () => {
+ // First file succeeds, second returns undefined
+ mockedParseSourceCodeDefinitions
+ .mockResolvedValueOnce("1--3 | export const x = 1")
+ .mockResolvedValueOnce(undefined)
+
+ const result = await generateFoldedFileContext(["/test/existing.ts", "/test/unsupported.txt"], {
+ cwd: "/test",
+ })
+
+ expect(result.filesProcessed).toBe(1)
+ expect(result.filesSkipped).toBe(1)
+ })
+
+ it("should skip files when parseSourceCodeDefinitions throws an error", async () => {
+ mockedParseSourceCodeDefinitions
+ .mockResolvedValueOnce("1--3 | export const x = 1")
+ .mockRejectedValueOnce(new Error("File not found"))
+
+ const result = await generateFoldedFileContext(["/test/existing.ts", "/test/non-existent.ts"], {
+ cwd: "/test",
+ })
+
+ expect(result.filesProcessed).toBe(1)
+ expect(result.filesSkipped).toBe(1)
+ })
+
+ it("should skip files when parseSourceCodeDefinitions returns error strings", async () => {
+ // Tree-sitter can return error strings for missing or denied files
+ // These should be treated as skipped, not embedded in the output
+ mockedParseSourceCodeDefinitions
+ .mockResolvedValueOnce("1--3 | export const x = 1")
+ .mockResolvedValueOnce("This file does not exist or you do not have permission to access it.")
+ .mockResolvedValueOnce("Unsupported file type: /test/file.xyz")
+
+ const result = await generateFoldedFileContext(["/test/valid.ts", "/test/missing.ts", "/test/file.xyz"], {
+ cwd: "/test",
+ })
+
+ // Only the first file should be processed, the other two return error strings
+ expect(result.filesProcessed).toBe(1)
+ expect(result.filesSkipped).toBe(2)
+
+ // The content should NOT contain the error messages
+ expect(result.content).not.toContain("does not exist")
+ expect(result.content).not.toContain("do not have permission")
+ expect(result.content).not.toContain("Unsupported file type")
+
+ // But it should contain the valid file's content
+ expect(result.content).toContain("## File Context: /test/valid.ts")
+ expect(result.content).toContain("export const x = 1")
+ })
+
+ it("should respect character budget limit", async () => {
+ // Create multiple files that would exceed a small budget
+ const longDefinitions = `1--3 | export function longFunctionName1()
+5--7 | export function longFunctionName2()
+9--11 | export function longFunctionName3()`
+
+ mockedParseSourceCodeDefinitions.mockResolvedValue(longDefinitions)
+
+ const result = await generateFoldedFileContext(["/test/file1.ts", "/test/file2.ts", "/test/file3.ts"], {
+ cwd: "/test",
+ maxCharacters: 200, // Small budget
+ })
+
+ expect(result.characterCount).toBeLessThanOrEqual(200)
+ // Some files should be skipped due to budget limit
+ expect(result.filesSkipped).toBeGreaterThan(0)
+ })
+
+ it("should handle Python files with its own system-reminder block", async () => {
+ const mockDefinitions = `1--2 | def greet(name)
+4--12 | class Person`
+
+ mockedParseSourceCodeDefinitions.mockResolvedValue(mockDefinitions)
+
+ const result = await generateFoldedFileContext(["/test/person.py"], { cwd: "/test" })
+
+ expect(result.content).toContain("")
+ expect(result.content).toContain("## File Context: /test/person.py")
+ expect(result.content).toContain("def greet")
+ expect(result.content).toContain("class Person")
+ expect(result.filesProcessed).toBe(1)
+ })
+
+ it("should include file path in the File Context header", async () => {
+ mockedParseSourceCodeDefinitions.mockResolvedValue("1--3 | export function helper()")
+
+ const result = await generateFoldedFileContext(["/test/src/utils/helpers.ts"], { cwd: "/test" })
+
+ // The path should appear in the File Context header
+ expect(result.content).toContain("## File Context: /test/src/utils/helpers.ts")
+ })
+
+ it("should generate separate system-reminder blocks for multiple files", async () => {
+ mockedParseSourceCodeDefinitions
+ .mockResolvedValueOnce("1--3 | export async function fetchData(url: string): Promise")
+ .mockResolvedValueOnce("1--4 | export interface DataModel")
+
+ const result = await generateFoldedFileContext(["/test/api.ts", "/test/models.ts"], { cwd: "/test" })
+
+ // Each file should have its own block
+ const systemReminderMatches = result.content.match(//g)
+ expect(systemReminderMatches).toHaveLength(2)
+
+ // sections array should have separate entries for each file
+ expect(result.sections).toHaveLength(2)
+ expect(result.sections[0]).toContain("## File Context: /test/api.ts")
+ expect(result.sections[1]).toContain("## File Context: /test/models.ts")
+
+ expect(result.content).toContain("## File Context: /test/api.ts")
+ expect(result.content).toContain("## File Context: /test/models.ts")
+ expect(result.content).toContain("fetchData")
+ expect(result.content).toContain("interface DataModel")
+ expect(result.filesProcessed).toBe(2)
+ })
+
+ it("should truncate content when approaching character limit", async () => {
+ // Create a definition that would fit but is close to the limit
+ const longDefinitions = "1--3 | " + "x".repeat(300)
+
+ mockedParseSourceCodeDefinitions.mockResolvedValue(longDefinitions)
+
+ const result = await generateFoldedFileContext(["/test/file1.ts", "/test/file2.ts"], {
+ cwd: "/test",
+ maxCharacters: 350, // First file will fit, second will be truncated
+ })
+
+ // Content should include truncation marker if truncation happened
+ expect(result.filesProcessed + result.filesSkipped).toBe(2)
+ })
+ })
+
+ describe("summarizeConversation with foldedFileContext", () => {
+ beforeEach(() => {
+ if (!TelemetryService.hasInstance()) {
+ TelemetryService.createInstance([])
+ }
+ })
+
+ // Mock API handler for testing
+ class MockApiHandler extends BaseProvider {
+ createMessage(): any {
+ const mockStream = {
+ async *[Symbol.asyncIterator]() {
+ yield { type: "text", text: "Mock summary of the conversation" }
+ yield { type: "usage", inputTokens: 100, outputTokens: 50, totalCost: 0.01 }
+ },
+ }
+ return mockStream
+ }
+
+ getModel(): { id: string; info: ModelInfo } {
+ return {
+ id: "test-model",
+ info: {
+ contextWindow: 100000,
+ maxTokens: 50000,
+ supportsPromptCache: true,
+ supportsImages: false,
+ inputPrice: 0,
+ outputPrice: 0,
+ description: "Test model",
+ },
+ }
+ }
+
+ override async countTokens(content: Array): Promise {
+ let tokens = 0
+ for (const block of content) {
+ if (block.type === "text") {
+ tokens += Math.ceil(block.text.length / 4)
+ }
+ }
+ return tokens
+ }
+ }
+
+ it("should include folded file context with each file as a separate content block", async () => {
+ const { summarizeConversation } = await import("../index")
+
+ const mockApiHandler = new MockApiHandler()
+ const taskId = "test-task-id"
+
+ const messages: any[] = [
+ { role: "user", content: "First message" },
+ { 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" },
+ ]
+
+ // Mock generateFoldedFileContext to return the expected folded sections
+ const mockFoldedSections = [
+ `
+## File Context: src/user.ts
+1--5 | export interface User
+7--12 | export function createUser(name: string): User
+14--28 | export class UserService
+`,
+ `
+## File Context: src/api.ts
+1--3 | export async function fetchData(url: string): Promise
+`,
+ ]
+
+ mockedGenerateFoldedFileContext.mockResolvedValue({
+ content: mockFoldedSections.join("\n"),
+ sections: mockFoldedSections,
+ filesProcessed: 2,
+ filesSkipped: 0,
+ characterCount: mockFoldedSections.join("\n").length,
+ })
+
+ const filesReadByRoo = ["src/user.ts", "src/api.ts"]
+ const cwd = "/test/project"
+
+ const result = await summarizeConversation({
+ messages,
+ apiHandler: mockApiHandler,
+ systemPrompt: "System prompt",
+ taskId,
+ isAutomaticTrigger: false,
+ filesReadByRoo,
+ cwd,
+ })
+
+ // Verify generateFoldedFileContext was called with the right arguments
+ expect(mockedGenerateFoldedFileContext).toHaveBeenCalledWith(filesReadByRoo, {
+ cwd,
+ rooIgnoreController: undefined,
+ })
+
+ // Verify the summary was created
+ expect(result.summary).toBeDefined()
+ expect(result.messages.length).toBeGreaterThan(0)
+
+ // Find the summary message
+ const summaryMessage = result.messages.find((msg: any) => msg.isSummary)
+ expect(summaryMessage).toBeDefined()
+
+ // Each file should have its own content block
+ const contentArray = summaryMessage!.content as any[]
+
+ // Find the content blocks containing file contexts
+ const userFileBlock = contentArray.find(
+ (block: any) => block.type === "text" && block.text?.includes("## File Context: src/user.ts"),
+ )
+ const apiFileBlock = contentArray.find(
+ (block: any) => block.type === "text" && block.text?.includes("## File Context: src/api.ts"),
+ )
+
+ expect(userFileBlock).toBeDefined()
+ expect(apiFileBlock).toBeDefined()
+
+ // Each file block should have its own tags
+ expect(userFileBlock.text).toContain("")
+ expect(userFileBlock.text).toContain("export interface User")
+
+ expect(apiFileBlock.text).toContain("")
+ expect(apiFileBlock.text).toContain("fetchData")
+ })
+
+ it("should not include file context section when filesReadByRoo is empty", async () => {
+ const { summarizeConversation } = await import("../index")
+
+ const mockApiHandler = new MockApiHandler()
+ const taskId = "test-task-id-2"
+
+ const messages: any[] = [
+ { role: "user", content: "First message" },
+ { 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" },
+ ]
+
+ // Reset the mock to ensure clean state
+ mockedGenerateFoldedFileContext.mockClear()
+
+ const result = await summarizeConversation({
+ messages,
+ apiHandler: mockApiHandler,
+ systemPrompt: "System prompt",
+ taskId,
+ isAutomaticTrigger: false,
+ filesReadByRoo: [],
+ cwd: "/test/project",
+ })
+
+ // generateFoldedFileContext should NOT be called when filesReadByRoo is empty
+ expect(mockedGenerateFoldedFileContext).not.toHaveBeenCalled()
+
+ // Find the summary message
+ const summaryMessage = result.messages.find((msg: any) => msg.isSummary)
+ expect(summaryMessage).toBeDefined()
+
+ // The summary content should NOT contain any file context blocks
+ const contentArray = summaryMessage!.content as any[]
+ const fileContextBlock = contentArray.find(
+ (block: any) => block.type === "text" && block.text?.includes("## File Context"),
+ )
+ expect(fileContextBlock).toBeUndefined()
+ })
+ })
+})
diff --git a/src/core/condense/__tests__/index.spec.ts b/src/core/condense/__tests__/index.spec.ts
index ef5af01243..75190985db 100644
--- a/src/core/condense/__tests__/index.spec.ts
+++ b/src/core/condense/__tests__/index.spec.ts
@@ -11,10 +11,10 @@ import { maybeRemoveImageBlocks } from "../../../api/transform/image-cleaning"
import {
summarizeConversation,
getMessagesSinceLastSummary,
- getKeepMessagesWithToolBlocks,
getEffectiveApiHistory,
cleanupAfterTruncation,
- N_MESSAGES_TO_KEEP,
+ extractCommandBlocks,
+ injectSyntheticToolResults,
} from "../index"
vi.mock("../../../api/transform/image-cleaning", () => ({
@@ -30,557 +30,213 @@ vi.mock("@roo-code/telemetry", () => ({
}))
const taskId = "test-task-id"
-const DEFAULT_PREV_CONTEXT_TOKENS = 1000
-describe("getKeepMessagesWithToolBlocks", () => {
- it("should return keepMessages without tool blocks when no tool_result blocks in first kept message", () => {
- const messages: ApiMessage[] = [
- { role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Hi there", ts: 2 },
- { role: "user", content: "How are you?", ts: 3 },
- { role: "assistant", content: "I'm good", ts: 4 },
- { role: "user", content: "What's new?", ts: 5 },
- ]
+describe("extractCommandBlocks", () => {
+ it("should extract command blocks from string content", () => {
+ const message: ApiMessage = {
+ role: "user",
+ content: 'Some text /prr #123 more text',
+ }
- const result = getKeepMessagesWithToolBlocks(messages, 3)
-
- expect(result.keepMessages).toHaveLength(3)
- expect(result.toolUseBlocksToPreserve).toHaveLength(0)
+ const result = extractCommandBlocks(message)
+ expect(result).toBe('/prr #123')
})
- it("should return all messages when messages.length <= keepCount", () => {
- const messages: ApiMessage[] = [
- { role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Hi there", ts: 2 },
- ]
+ it("should extract multiple command blocks", () => {
+ const message: ApiMessage = {
+ role: "user",
+ content: '/prr #123 text /mode code',
+ }
- const result = getKeepMessagesWithToolBlocks(messages, 3)
-
- expect(result.keepMessages).toEqual(messages)
- expect(result.toolUseBlocksToPreserve).toHaveLength(0)
+ const result = extractCommandBlocks(message)
+ expect(result).toBe('/prr #123\n/mode code')
})
- it("should preserve tool_use blocks when first kept message has tool_result blocks", () => {
- const toolUseBlock = {
- type: "tool_use" as const,
- id: "toolu_123",
- name: "read_file",
- input: { path: "test.txt" },
- }
- const toolResultBlock = {
- type: "tool_result" as const,
- tool_use_id: "toolu_123",
- content: "file contents",
+ it("should extract command blocks from array content", () => {
+ const message: ApiMessage = {
+ role: "user",
+ content: [
+ { type: "text", text: "Some user text" },
+ { type: "text", text: 'Help content' },
+ ],
}
- const messages: ApiMessage[] = [
- { role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Let me read that file", ts: 2 },
- { role: "user", content: "Please continue", ts: 3 },
- {
- role: "assistant",
- content: [{ type: "text" as const, text: "Reading file..." }, toolUseBlock],
- ts: 4,
- },
- {
- role: "user",
- content: [toolResultBlock, { type: "text" as const, text: "Continue" }],
- ts: 5,
- },
- { role: "assistant", content: "Got it, the file says...", ts: 6 },
- { role: "user", content: "Thanks", ts: 7 },
- ]
-
- const result = getKeepMessagesWithToolBlocks(messages, 3)
-
- // keepMessages should be the last 3 messages
- expect(result.keepMessages).toHaveLength(3)
- expect(result.keepMessages[0].ts).toBe(5)
- expect(result.keepMessages[1].ts).toBe(6)
- expect(result.keepMessages[2].ts).toBe(7)
-
- // Should preserve the tool_use block from the preceding assistant message
- expect(result.toolUseBlocksToPreserve).toHaveLength(1)
- expect(result.toolUseBlocksToPreserve[0]).toEqual(toolUseBlock)
+ const result = extractCommandBlocks(message)
+ expect(result).toBe('Help content')
})
- it("should not preserve tool_use blocks when first kept message is assistant role", () => {
- const toolUseBlock = {
- type: "tool_use" as const,
- id: "toolu_123",
- name: "read_file",
- input: { path: "test.txt" },
+ it("should return empty string when no command blocks found", () => {
+ const message: ApiMessage = {
+ role: "user",
+ content: "Just regular text without commands",
}
- const messages: ApiMessage[] = [
- { role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Hi there", ts: 2 },
- { role: "user", content: "Please read", ts: 3 },
- {
- role: "assistant",
- content: [{ type: "text" as const, text: "Reading..." }, toolUseBlock],
- ts: 4,
- },
- { role: "user", content: "Continue", ts: 5 },
- { role: "assistant", content: "Done", ts: 6 },
- ]
-
- const result = getKeepMessagesWithToolBlocks(messages, 3)
-
- // First kept message is assistant, not user with tool_result
- expect(result.keepMessages).toHaveLength(3)
- expect(result.keepMessages[0].role).toBe("assistant")
- expect(result.toolUseBlocksToPreserve).toHaveLength(0)
+ const result = extractCommandBlocks(message)
+ expect(result).toBe("")
})
- it("should not preserve tool_use blocks when first kept user message has string content", () => {
- const messages: ApiMessage[] = [
- { role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Hi there", ts: 2 },
- { role: "user", content: "How are you?", ts: 3 },
- { role: "assistant", content: "Good", ts: 4 },
- { role: "user", content: "Simple text message", ts: 5 }, // String content, not array
- { role: "assistant", content: "Response", ts: 6 },
- { role: "user", content: "More text", ts: 7 },
- ]
+ it("should handle multiline command blocks", () => {
+ const message: ApiMessage = {
+ role: "user",
+ content: `
+Line 1
+Line 2
+`,
+ }
- const result = getKeepMessagesWithToolBlocks(messages, 3)
-
- expect(result.keepMessages).toHaveLength(3)
- expect(result.toolUseBlocksToPreserve).toHaveLength(0)
+ const result = extractCommandBlocks(message)
+ expect(result).toContain("Line 1")
+ expect(result).toContain("Line 2")
})
- it("should handle multiple tool_use blocks that need to be preserved", () => {
- const toolUseBlock1 = {
- type: "tool_use" as const,
- id: "toolu_123",
- name: "read_file",
- input: { path: "file1.txt" },
- }
- const toolUseBlock2 = {
- type: "tool_use" as const,
- id: "toolu_456",
- name: "read_file",
- input: { path: "file2.txt" },
- }
- const toolResultBlock1 = {
- type: "tool_result" as const,
- tool_use_id: "toolu_123",
- content: "contents 1",
- }
- const toolResultBlock2 = {
- type: "tool_result" as const,
- tool_use_id: "toolu_456",
- content: "contents 2",
+ it("should handle command blocks with attributes", () => {
+ const message: ApiMessage = {
+ role: "user",
+ content: 'content',
}
+ const result = extractCommandBlocks(message)
+ expect(result).toContain('name="test"')
+ expect(result).toContain('attr1="value1"')
+ })
+})
+
+describe("injectSyntheticToolResults", () => {
+ it("should return messages unchanged when no orphan tool_calls exist", () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Hello", ts: 1 },
{
role: "assistant",
- content: [{ type: "text" as const, text: "Reading files..." }, toolUseBlock1, toolUseBlock2],
+ content: [{ type: "tool_use", id: "tool-1", name: "read_file", input: { path: "test.ts" } }],
ts: 2,
},
{
role: "user",
- content: [toolResultBlock1, toolResultBlock2],
+ content: [{ type: "tool_result", tool_use_id: "tool-1", content: "file contents" }],
ts: 3,
},
- { role: "assistant", content: "Got both files", ts: 4 },
- { role: "user", content: "Thanks", ts: 5 },
]
- const result = getKeepMessagesWithToolBlocks(messages, 3)
-
- // Should preserve both tool_use blocks
- expect(result.toolUseBlocksToPreserve).toHaveLength(2)
- expect(result.toolUseBlocksToPreserve).toContainEqual(toolUseBlock1)
- expect(result.toolUseBlocksToPreserve).toContainEqual(toolUseBlock2)
+ const result = injectSyntheticToolResults(messages)
+ expect(result).toEqual(messages)
})
- it("should not preserve tool_use blocks when preceding message has no tool_use blocks", () => {
- const toolResultBlock = {
- type: "tool_result" as const,
- tool_use_id: "toolu_123",
- content: "file contents",
- }
-
+ it("should inject synthetic tool_result for orphan tool_call", () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Plain text response", ts: 2 }, // No tool_use blocks
- {
- role: "user",
- content: [toolResultBlock], // Has tool_result but preceding message has no tool_use
- ts: 3,
- },
- { role: "assistant", content: "Response", ts: 4 },
- { role: "user", content: "Thanks", ts: 5 },
- ]
-
- const result = getKeepMessagesWithToolBlocks(messages, 3)
-
- expect(result.keepMessages).toHaveLength(3)
- expect(result.toolUseBlocksToPreserve).toHaveLength(0)
- })
-
- it("should handle edge case when startIndex - 1 is negative", () => {
- const toolResultBlock = {
- type: "tool_result" as const,
- tool_use_id: "toolu_123",
- content: "file contents",
- }
-
- // Only 3 messages total, so startIndex = 0 and precedingIndex would be -1
- const messages: ApiMessage[] = [
- {
- role: "user",
- content: [toolResultBlock],
- ts: 1,
- },
- { role: "assistant", content: "Response", ts: 2 },
- { role: "user", content: "Thanks", ts: 3 },
- ]
-
- const result = getKeepMessagesWithToolBlocks(messages, 3)
-
- expect(result.keepMessages).toEqual(messages)
- expect(result.toolUseBlocksToPreserve).toHaveLength(0)
- })
-
- it("should preserve reasoning blocks alongside tool_use blocks for DeepSeek/Z.ai interleaved thinking", () => {
- const reasoningBlock = {
- type: "reasoning" as const,
- text: "Let me think about this step by step...",
- }
- const toolUseBlock = {
- type: "tool_use" as const,
- id: "toolu_deepseek_123",
- name: "read_file",
- input: { path: "test.txt" },
- }
- const toolResultBlock = {
- type: "tool_result" as const,
- tool_use_id: "toolu_deepseek_123",
- content: "file contents",
- }
-
- const messages: ApiMessage[] = [
- { role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Let me help", ts: 2 },
- { role: "user", content: "Please read the file", ts: 3 },
- {
- role: "assistant",
- // DeepSeek stores reasoning as content blocks alongside tool_use
- content: [reasoningBlock as any, { type: "text" as const, text: "Reading file..." }, toolUseBlock],
- ts: 4,
- },
- {
- role: "user",
- content: [toolResultBlock, { type: "text" as const, text: "Continue" }],
- ts: 5,
- },
- { role: "assistant", content: "Got it, the file says...", ts: 6 },
- { role: "user", content: "Thanks", ts: 7 },
- ]
-
- const result = getKeepMessagesWithToolBlocks(messages, 3)
-
- // keepMessages should be the last 3 messages
- expect(result.keepMessages).toHaveLength(3)
- expect(result.keepMessages[0].ts).toBe(5)
-
- // Should preserve the tool_use block
- expect(result.toolUseBlocksToPreserve).toHaveLength(1)
- expect(result.toolUseBlocksToPreserve[0]).toEqual(toolUseBlock)
-
- // Should preserve the reasoning block for DeepSeek/Z.ai interleaved thinking
- expect(result.reasoningBlocksToPreserve).toHaveLength(1)
- expect((result.reasoningBlocksToPreserve[0] as any).type).toBe("reasoning")
- expect((result.reasoningBlocksToPreserve[0] as any).text).toBe("Let me think about this step by step...")
- })
-
- it("should return empty reasoningBlocksToPreserve when no reasoning blocks present", () => {
- const toolUseBlock = {
- type: "tool_use" as const,
- id: "toolu_123",
- name: "read_file",
- input: { path: "test.txt" },
- }
- const toolResultBlock = {
- type: "tool_result" as const,
- tool_use_id: "toolu_123",
- content: "file contents",
- }
-
- const messages: ApiMessage[] = [
- { role: "user", content: "Hello", ts: 1 },
- {
- role: "assistant",
- // No reasoning block, just text and tool_use
- content: [{ type: "text" as const, text: "Reading file..." }, toolUseBlock],
- ts: 2,
- },
- {
- role: "user",
- content: [toolResultBlock],
- ts: 3,
- },
- { role: "assistant", content: "Done", ts: 4 },
- { role: "user", content: "Thanks", ts: 5 },
- ]
-
- const result = getKeepMessagesWithToolBlocks(messages, 3)
-
- expect(result.toolUseBlocksToPreserve).toHaveLength(1)
- expect(result.reasoningBlocksToPreserve).toHaveLength(0)
- })
-
- it("should preserve tool_use when tool_result is in 2nd kept message and tool_use is 2 messages before boundary", () => {
- const toolUseBlock = {
- type: "tool_use" as const,
- id: "toolu_second_kept",
- name: "read_file",
- input: { path: "test.txt" },
- }
- const toolResultBlock = {
- type: "tool_result" as const,
- tool_use_id: "toolu_second_kept",
- content: "file contents",
- }
-
- const messages: ApiMessage[] = [
- { role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Let me help", ts: 2 },
- {
- role: "assistant",
- content: [{ type: "text" as const, text: "Reading file..." }, toolUseBlock],
- ts: 3,
- },
- { role: "user", content: "Some other message", ts: 4 },
- { role: "assistant", content: "First kept message", ts: 5 },
- {
- role: "user",
- content: [toolResultBlock, { type: "text" as const, text: "Continue" }],
- ts: 6,
- },
- { role: "assistant", content: "Third kept message", ts: 7 },
- ]
-
- const result = getKeepMessagesWithToolBlocks(messages, 3)
-
- // keepMessages should be the last 3 messages (ts: 5, 6, 7)
- expect(result.keepMessages).toHaveLength(3)
- expect(result.keepMessages[0].ts).toBe(5)
- expect(result.keepMessages[1].ts).toBe(6)
- expect(result.keepMessages[2].ts).toBe(7)
-
- // Should preserve the tool_use block from message at ts:3 (2 messages before boundary)
- expect(result.toolUseBlocksToPreserve).toHaveLength(1)
- expect(result.toolUseBlocksToPreserve[0]).toEqual(toolUseBlock)
- })
-
- it("should preserve tool_use when tool_result is in 3rd kept message and tool_use is at boundary edge", () => {
- const toolUseBlock = {
- type: "tool_use" as const,
- id: "toolu_third_kept",
- name: "search",
- input: { query: "test" },
- }
- const toolResultBlock = {
- type: "tool_result" as const,
- tool_use_id: "toolu_third_kept",
- content: "search results",
- }
-
- const messages: ApiMessage[] = [
- { role: "user", content: "Start", ts: 1 },
- {
- role: "assistant",
- content: [{ type: "text" as const, text: "Searching..." }, toolUseBlock],
- ts: 2,
- },
- { role: "user", content: "First kept message", ts: 3 },
- { role: "assistant", content: "Second kept message", ts: 4 },
- {
- role: "user",
- content: [toolResultBlock, { type: "text" as const, text: "Done" }],
- ts: 5,
- },
- ]
-
- const result = getKeepMessagesWithToolBlocks(messages, 3)
-
- // keepMessages should be the last 3 messages (ts: 3, 4, 5)
- expect(result.keepMessages).toHaveLength(3)
- expect(result.keepMessages[0].ts).toBe(3)
- expect(result.keepMessages[1].ts).toBe(4)
- expect(result.keepMessages[2].ts).toBe(5)
-
- // Should preserve the tool_use block from message at ts:2 (at the search boundary edge)
- expect(result.toolUseBlocksToPreserve).toHaveLength(1)
- expect(result.toolUseBlocksToPreserve[0]).toEqual(toolUseBlock)
- })
-
- it("should preserve multiple tool_uses when tool_results are in different kept messages", () => {
- const toolUseBlock1 = {
- type: "tool_use" as const,
- id: "toolu_multi_1",
- name: "read_file",
- input: { path: "file1.txt" },
- }
- const toolUseBlock2 = {
- type: "tool_use" as const,
- id: "toolu_multi_2",
- name: "read_file",
- input: { path: "file2.txt" },
- }
- const toolResultBlock1 = {
- type: "tool_result" as const,
- tool_use_id: "toolu_multi_1",
- content: "contents 1",
- }
- const toolResultBlock2 = {
- type: "tool_result" as const,
- tool_use_id: "toolu_multi_2",
- content: "contents 2",
- }
-
- const messages: ApiMessage[] = [
- { role: "user", content: "Start", ts: 1 },
- {
- role: "assistant",
- content: [{ type: "text" as const, text: "Reading file 1..." }, toolUseBlock1],
- ts: 2,
- },
- { role: "user", content: "Some message", ts: 3 },
- {
- role: "assistant",
- content: [{ type: "text" as const, text: "Reading file 2..." }, toolUseBlock2],
- ts: 4,
- },
- {
- role: "user",
- content: [toolResultBlock1, { type: "text" as const, text: "First result" }],
- ts: 5,
- },
- {
- role: "user",
- content: [toolResultBlock2, { type: "text" as const, text: "Second result" }],
- ts: 6,
- },
- { role: "assistant", content: "Got both files", ts: 7 },
- ]
-
- const result = getKeepMessagesWithToolBlocks(messages, 3)
-
- // keepMessages should be the last 3 messages (ts: 5, 6, 7)
- expect(result.keepMessages).toHaveLength(3)
-
- // Should preserve both tool_use blocks
- expect(result.toolUseBlocksToPreserve).toHaveLength(2)
- expect(result.toolUseBlocksToPreserve).toContainEqual(toolUseBlock1)
- expect(result.toolUseBlocksToPreserve).toContainEqual(toolUseBlock2)
- })
-
- it("should not crash when tool_result references tool_use beyond search boundary", () => {
- const toolResultBlock = {
- type: "tool_result" as const,
- tool_use_id: "toolu_beyond_boundary",
- content: "result",
- }
-
- // Tool_use is at ts:1, but with N_MESSAGES_TO_KEEP=3, we only search back 3 messages
- // from startIndex-1. StartIndex is 7 (messages.length=10, keepCount=3, startIndex=7).
- // So we search from index 6 down to index 4 (7-1 down to 7-3).
- // The tool_use at index 0 (ts:1) is beyond the search boundary.
- const messages: ApiMessage[] = [
{
role: "assistant",
content: [
- { type: "text" as const, text: "Way back..." },
- {
- type: "tool_use" as const,
- id: "toolu_beyond_boundary",
- name: "old_tool",
- input: {},
- },
+ { type: "tool_use", id: "tool-orphan", name: "attempt_completion", input: { result: "Done" } },
],
- ts: 1,
+ ts: 2,
},
- { role: "user", content: "Message 2", ts: 2 },
- { role: "assistant", content: "Message 3", ts: 3 },
- { role: "user", content: "Message 4", ts: 4 },
- { role: "assistant", content: "Message 5", ts: 5 },
- { role: "user", content: "Message 6", ts: 6 },
- { role: "assistant", content: "Message 7", ts: 7 },
- {
- role: "user",
- content: [toolResultBlock],
- ts: 8,
- },
- { role: "assistant", content: "Message 9", ts: 9 },
- { role: "user", content: "Message 10", ts: 10 },
+ // No tool_result for tool-orphan
]
- // Should not crash
- const result = getKeepMessagesWithToolBlocks(messages, 3)
+ const result = injectSyntheticToolResults(messages)
- // keepMessages should be the last 3 messages
- expect(result.keepMessages).toHaveLength(3)
- expect(result.keepMessages[0].ts).toBe(8)
- expect(result.keepMessages[1].ts).toBe(9)
- expect(result.keepMessages[2].ts).toBe(10)
+ expect(result.length).toBe(3)
+ expect(result[2].role).toBe("user")
- // Should not preserve the tool_use since it's beyond the search boundary
- expect(result.toolUseBlocksToPreserve).toHaveLength(0)
+ const content = result[2].content as any[]
+ expect(content.length).toBe(1)
+ expect(content[0].type).toBe("tool_result")
+ expect(content[0].tool_use_id).toBe("tool-orphan")
+ expect(content[0].content).toBe("Context condensation triggered. Tool execution deferred.")
})
- it("should not duplicate tool_use blocks when same tool_result ID appears multiple times", () => {
- const toolUseBlock = {
- type: "tool_use" as const,
- id: "toolu_duplicate",
- name: "read_file",
- input: { path: "test.txt" },
- }
- const toolResultBlock1 = {
- type: "tool_result" as const,
- tool_use_id: "toolu_duplicate",
- content: "result 1",
- }
- const toolResultBlock2 = {
- type: "tool_result" as const,
- tool_use_id: "toolu_duplicate",
- content: "result 2",
- }
-
+ it("should inject synthetic tool_results for multiple orphan tool_calls", () => {
const messages: ApiMessage[] = [
- { role: "user", content: "Start", ts: 1 },
+ { role: "user", content: "Hello", ts: 1 },
{
role: "assistant",
- content: [{ type: "text" as const, text: "Using tool..." }, toolUseBlock],
+ content: [
+ { type: "tool_use", id: "tool-1", name: "read_file", input: { path: "test.ts" } },
+ { type: "tool_use", id: "tool-2", name: "write_file", input: { path: "out.ts", content: "code" } },
+ ],
+ ts: 2,
+ },
+ // No tool_results for either
+ ]
+
+ const result = injectSyntheticToolResults(messages)
+
+ expect(result.length).toBe(3)
+ const content = result[2].content as any[]
+ expect(content.length).toBe(2)
+ expect(content[0].tool_use_id).toBe("tool-1")
+ expect(content[1].tool_use_id).toBe("tool-2")
+ })
+
+ it("should only inject for orphan tool_calls, not matched ones", () => {
+ const messages: ApiMessage[] = [
+ { role: "user", content: "Hello", ts: 1 },
+ {
+ role: "assistant",
+ content: [
+ { type: "tool_use", id: "matched-tool", name: "read_file", input: { path: "test.ts" } },
+ { type: "tool_use", id: "orphan-tool", name: "attempt_completion", input: { result: "Done" } },
+ ],
ts: 2,
},
{
role: "user",
- content: [toolResultBlock1],
+ content: [{ type: "tool_result", tool_use_id: "matched-tool", content: "file contents" }],
ts: 3,
},
- { role: "assistant", content: "Processing", ts: 4 },
+ // No tool_result for orphan-tool
+ ]
+
+ const result = injectSyntheticToolResults(messages)
+
+ expect(result.length).toBe(4)
+ const syntheticContent = result[3].content as any[]
+ expect(syntheticContent.length).toBe(1)
+ expect(syntheticContent[0].tool_use_id).toBe("orphan-tool")
+ })
+
+ it("should handle messages with string content (no tool_use/tool_result)", () => {
+ const messages: ApiMessage[] = [
+ { role: "user", content: "Hello", ts: 1 },
+ { role: "assistant", content: "Hi there!", ts: 2 },
+ ]
+
+ const result = injectSyntheticToolResults(messages)
+ expect(result).toEqual(messages)
+ })
+
+ it("should handle empty messages array", () => {
+ const result = injectSyntheticToolResults([])
+ expect(result).toEqual([])
+ })
+
+ it("should handle tool_results spread across multiple user messages", () => {
+ const messages: ApiMessage[] = [
+ { role: "user", content: "Hello", ts: 1 },
+ {
+ role: "assistant",
+ content: [
+ { type: "tool_use", id: "tool-1", name: "read_file", input: { path: "a.ts" } },
+ { type: "tool_use", id: "tool-2", name: "read_file", input: { path: "b.ts" } },
+ ],
+ ts: 2,
+ },
{
role: "user",
- content: [toolResultBlock2], // Same tool_use_id as first result
- ts: 5,
+ content: [{ type: "tool_result", tool_use_id: "tool-1", content: "contents a" }],
+ ts: 3,
+ },
+ {
+ role: "user",
+ content: [{ type: "tool_result", tool_use_id: "tool-2", content: "contents b" }],
+ ts: 4,
},
]
- const result = getKeepMessagesWithToolBlocks(messages, 3)
-
- // keepMessages should be the last 3 messages (ts: 3, 4, 5)
- expect(result.keepMessages).toHaveLength(3)
-
- // Should only preserve the tool_use block once, not twice
- expect(result.toolUseBlocksToPreserve).toHaveLength(1)
- expect(result.toolUseBlocksToPreserve[0]).toEqual(toolUseBlock)
+ const result = injectSyntheticToolResults(messages)
+ // Both tool_uses have matching tool_results, no injection needed
+ expect(result).toEqual(messages)
})
})
@@ -596,38 +252,36 @@ describe("getMessagesSinceLastSummary", () => {
expect(result).toEqual(messages)
})
- it("should return messages since the last summary with original first user message", () => {
+ it("should return messages since the last summary", () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Hello", ts: 1 },
{ role: "assistant", content: "Hi there", ts: 2 },
- { role: "assistant", content: "Summary of conversation", ts: 3, isSummary: true },
- { role: "user", content: "How are you?", ts: 4 },
- { role: "assistant", content: "I'm good", ts: 5 },
+ { role: "user", content: "Summary of conversation", ts: 3, isSummary: true },
+ { role: "assistant", content: "How are you?", ts: 4 },
+ { role: "user", content: "I'm good", ts: 5 },
]
const result = getMessagesSinceLastSummary(messages)
expect(result).toEqual([
- { role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Summary of conversation", ts: 3, isSummary: true },
- { role: "user", content: "How are you?", ts: 4 },
- { role: "assistant", content: "I'm good", ts: 5 },
+ { role: "user", content: "Summary of conversation", ts: 3, isSummary: true },
+ { role: "assistant", content: "How are you?", ts: 4 },
+ { role: "user", content: "I'm good", ts: 5 },
])
})
- it("should handle multiple summary messages and return since the last one with original first user message", () => {
+ it("should handle multiple summary messages and return since the last one", () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "First summary", ts: 2, isSummary: true },
- { role: "user", content: "How are you?", ts: 3 },
- { role: "assistant", content: "Second summary", ts: 4, isSummary: true },
- { role: "user", content: "What's new?", ts: 5 },
+ { role: "user", content: "First summary", ts: 2, isSummary: true },
+ { role: "assistant", content: "How are you?", ts: 3 },
+ { role: "user", content: "Second summary", ts: 4, isSummary: true },
+ { role: "assistant", content: "What's new?", ts: 5 },
]
const result = getMessagesSinceLastSummary(messages)
expect(result).toEqual([
- { role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Second summary", ts: 4, isSummary: true },
- { role: "user", content: "What's new?", ts: 5 },
+ { role: "user", content: "Second summary", ts: 4, isSummary: true },
+ { role: "assistant", content: "What's new?", ts: 5 },
])
})
@@ -635,6 +289,383 @@ describe("getMessagesSinceLastSummary", () => {
const result = getMessagesSinceLastSummary([])
expect(result).toEqual([])
})
+
+ it("should return messages from user summary (fresh start model)", () => {
+ const messages: ApiMessage[] = [
+ { role: "user", content: "Hello", ts: 1, condenseParent: "cond-1" },
+ { role: "assistant", content: "Hi there", ts: 2, condenseParent: "cond-1" },
+ { role: "user", content: "Summary content", ts: 3, isSummary: true, condenseId: "cond-1" },
+ { role: "assistant", content: "Response after summary", ts: 4 },
+ ]
+
+ const result = getMessagesSinceLastSummary(messages)
+ expect(result[0].isSummary).toBe(true)
+ expect(result[0].role).toBe("user")
+ })
+})
+
+describe("getEffectiveApiHistory", () => {
+ it("should return only summary when summary exists (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: "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 - orphaned condenseParent)", () => {
+ 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)
+ })
+
+ it("should filter out truncated messages within summary range", () => {
+ const condenseId = "cond-1"
+ const truncationId = "trunc-1"
+ const messages: ApiMessage[] = [
+ { role: "user", content: "First", condenseParent: condenseId },
+ {
+ role: "user",
+ content: [{ type: "text", text: "Summary" }],
+ isSummary: true,
+ condenseId,
+ },
+ { role: "assistant", content: "Response", truncationParent: truncationId },
+ {
+ role: "assistant",
+ content: [{ type: "text", text: "..." }],
+ isTruncationMarker: true,
+ truncationId,
+ },
+ { role: "user", content: "After truncation" },
+ ]
+
+ const result = getEffectiveApiHistory(messages)
+
+ // Summary + truncation marker + after truncation (the truncated response is filtered out)
+ expect(result).toHaveLength(3)
+ expect(result[0].isSummary).toBe(true)
+ expect(result[1].isTruncationMarker).toBe(true)
+ expect(result[2].content).toBe("After truncation")
+ })
+
+ it("should filter out orphan tool_result blocks after fresh start condensation", () => {
+ const condenseId = "cond-1"
+ const messages: ApiMessage[] = [
+ { role: "user", content: "Hello", condenseParent: condenseId },
+ {
+ role: "assistant",
+ content: [
+ { type: "tool_use", id: "tool-orphan", name: "attempt_completion", input: { result: "Done" } },
+ ],
+ condenseParent: condenseId,
+ },
+ // Summary comes after the tool_use (so tool_use is condensed away)
+ {
+ role: "user",
+ content: [{ type: "text", text: "Summary content" }],
+ isSummary: true,
+ condenseId,
+ },
+ // This tool_result references a tool_use that was condensed away (orphan!)
+ {
+ role: "user",
+ content: [{ type: "tool_result", tool_use_id: "tool-orphan", content: "Rejected by user" }],
+ },
+ ]
+
+ const result = getEffectiveApiHistory(messages)
+
+ // Should only return the summary, orphan tool_result message should be filtered out
+ expect(result).toHaveLength(1)
+ expect(result[0].isSummary).toBe(true)
+ })
+
+ it("should keep tool_result blocks that have matching tool_use in fresh start", () => {
+ const condenseId = "cond-1"
+ const messages: ApiMessage[] = [
+ { role: "user", content: "Hello", condenseParent: condenseId },
+ {
+ role: "user",
+ content: [{ type: "text", text: "Summary content" }],
+ isSummary: true,
+ condenseId,
+ },
+ // This tool_use is AFTER the summary, so it's not condensed away
+ {
+ role: "assistant",
+ content: [{ type: "tool_use", id: "tool-valid", name: "read_file", input: { path: "test.ts" } }],
+ },
+ // This tool_result has a matching tool_use, so it should be kept
+ {
+ role: "user",
+ content: [{ type: "tool_result", tool_use_id: "tool-valid", content: "file contents" }],
+ },
+ ]
+
+ const result = getEffectiveApiHistory(messages)
+
+ // All messages after summary should be included
+ expect(result).toHaveLength(3)
+ expect(result[0].isSummary).toBe(true)
+ expect((result[1].content as any[])[0].id).toBe("tool-valid")
+ expect((result[2].content as any[])[0].tool_use_id).toBe("tool-valid")
+ })
+
+ it("should filter orphan tool_results but keep other content in mixed user message", () => {
+ const condenseId = "cond-1"
+ const messages: ApiMessage[] = [
+ { role: "user", content: "Hello", condenseParent: condenseId },
+ {
+ role: "assistant",
+ content: [
+ { type: "tool_use", id: "tool-orphan", name: "attempt_completion", input: { result: "Done" } },
+ ],
+ condenseParent: condenseId,
+ },
+ {
+ role: "user",
+ content: [{ type: "text", text: "Summary content" }],
+ isSummary: true,
+ condenseId,
+ },
+ // This tool_use is AFTER the summary
+ {
+ role: "assistant",
+ content: [{ type: "tool_use", id: "tool-valid", name: "read_file", input: { path: "test.ts" } }],
+ },
+ // Mixed content: one orphan tool_result and one valid tool_result
+ {
+ role: "user",
+ content: [
+ { type: "tool_result", tool_use_id: "tool-orphan", content: "Orphan result" },
+ { type: "tool_result", tool_use_id: "tool-valid", content: "Valid result" },
+ ],
+ },
+ ]
+
+ const result = getEffectiveApiHistory(messages)
+
+ // Summary + assistant with tool_use + filtered user message
+ expect(result).toHaveLength(3)
+ expect(result[0].isSummary).toBe(true)
+ // The user message should only contain the valid tool_result
+ const userContent = result[2].content as any[]
+ expect(userContent).toHaveLength(1)
+ expect(userContent[0].tool_use_id).toBe("tool-valid")
+ })
+
+ it("should handle multiple orphan tool_results in a single message", () => {
+ const condenseId = "cond-1"
+ const messages: ApiMessage[] = [
+ {
+ role: "assistant",
+ content: [
+ { type: "tool_use", id: "orphan-1", name: "read_file", input: { path: "a.ts" } },
+ { type: "tool_use", id: "orphan-2", name: "write_file", input: { path: "b.ts", content: "code" } },
+ ],
+ condenseParent: condenseId,
+ },
+ {
+ role: "user",
+ content: [{ type: "text", text: "Summary content" }],
+ isSummary: true,
+ condenseId,
+ },
+ // Multiple orphan tool_results - entire message should be removed
+ {
+ role: "user",
+ content: [
+ { type: "tool_result", tool_use_id: "orphan-1", content: "Result 1" },
+ { type: "tool_result", tool_use_id: "orphan-2", content: "Result 2" },
+ ],
+ },
+ ]
+
+ const result = getEffectiveApiHistory(messages)
+
+ // Only summary should remain
+ expect(result).toHaveLength(1)
+ expect(result[0].isSummary).toBe(true)
+ })
+
+ it("should preserve non-tool_result content in user messages", () => {
+ const condenseId = "cond-1"
+ const messages: ApiMessage[] = [
+ {
+ role: "assistant",
+ content: [
+ { type: "tool_use", id: "tool-orphan", name: "attempt_completion", input: { result: "Done" } },
+ ],
+ condenseParent: condenseId,
+ },
+ {
+ role: "user",
+ content: [{ type: "text", text: "Summary content" }],
+ isSummary: true,
+ condenseId,
+ },
+ // User message with text content and orphan tool_result
+ {
+ role: "user",
+ content: [
+ { type: "text", text: "User added some text" },
+ { type: "tool_result", tool_use_id: "tool-orphan", content: "Orphan result" },
+ ],
+ },
+ ]
+
+ const result = getEffectiveApiHistory(messages)
+
+ // Summary + user message with only text (orphan tool_result filtered)
+ expect(result).toHaveLength(2)
+ expect(result[0].isSummary).toBe(true)
+ const userContent = result[1].content as any[]
+ expect(userContent).toHaveLength(1)
+ expect(userContent[0].type).toBe("text")
+ expect(userContent[0].text).toBe("User added some text")
+ })
+})
+
+describe("cleanupAfterTruncation", () => {
+ it("should clear orphaned condenseParent references", () => {
+ const orphanedCondenseId = "deleted-summary"
+ const messages: ApiMessage[] = [
+ { role: "user", content: "First", condenseParent: orphanedCondenseId },
+ { role: "assistant", content: "Second", condenseParent: orphanedCondenseId },
+ { role: "user", content: "Third" },
+ ]
+
+ const result = cleanupAfterTruncation(messages)
+
+ expect(result[0].condenseParent).toBeUndefined()
+ expect(result[1].condenseParent).toBeUndefined()
+ expect(result[2].condenseParent).toBeUndefined()
+ })
+
+ it("should keep condenseParent when summary still exists", () => {
+ const condenseId = "existing-summary"
+ const messages: ApiMessage[] = [
+ { role: "user", content: "First", condenseParent: condenseId },
+ { role: "assistant", content: "Second", condenseParent: condenseId },
+ {
+ role: "user",
+ content: [{ type: "text", text: "Summary" }],
+ isSummary: true,
+ condenseId,
+ },
+ ]
+
+ const result = cleanupAfterTruncation(messages)
+
+ expect(result[0].condenseParent).toBe(condenseId)
+ expect(result[1].condenseParent).toBe(condenseId)
+ })
+
+ it("should clear orphaned truncationParent references", () => {
+ const orphanedTruncationId = "deleted-truncation"
+ const messages: ApiMessage[] = [
+ { role: "user", content: "First", truncationParent: orphanedTruncationId },
+ { role: "assistant", content: "Second" },
+ ]
+
+ const result = cleanupAfterTruncation(messages)
+
+ expect(result[0].truncationParent).toBeUndefined()
+ })
+
+ it("should keep truncationParent when marker still exists", () => {
+ const truncationId = "existing-truncation"
+ const messages: ApiMessage[] = [
+ { role: "user", content: "First", truncationParent: truncationId },
+ {
+ role: "assistant",
+ content: [{ type: "text", text: "..." }],
+ isTruncationMarker: true,
+ truncationId,
+ },
+ ]
+
+ const result = cleanupAfterTruncation(messages)
+
+ expect(result[0].truncationParent).toBe(truncationId)
+ })
+
+ it("should handle mixed orphaned and valid references", () => {
+ const validCondenseId = "valid-cond"
+ const orphanedCondenseId = "orphaned-cond"
+ const messages: ApiMessage[] = [
+ { role: "user", content: "First", condenseParent: orphanedCondenseId },
+ { role: "assistant", content: "Second", condenseParent: validCondenseId },
+ {
+ role: "user",
+ content: [{ type: "text", text: "Summary" }],
+ isSummary: true,
+ condenseId: validCondenseId,
+ },
+ ]
+
+ const result = cleanupAfterTruncation(messages)
+
+ expect(result[0].condenseParent).toBeUndefined() // orphaned, cleared
+ expect(result[1].condenseParent).toBe(validCondenseId) // valid, kept
+ })
})
describe("summarizeConversation", () => {
@@ -677,18 +708,14 @@ describe("summarizeConversation", () => {
const defaultSystemPrompt = "You are a helpful assistant."
it("should not summarize when there are not enough messages", async () => {
- const messages: ApiMessage[] = [
- { role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Hi there", ts: 2 },
- ]
+ const messages: ApiMessage[] = [{ role: "user", content: "Hello", ts: 1 }]
- const result = await summarizeConversation(
+ const result = await summarizeConversation({
messages,
- mockApiHandler,
- defaultSystemPrompt,
+ apiHandler: mockApiHandler,
+ systemPrompt: defaultSystemPrompt,
taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- )
+ })
expect(result.messages).toEqual(messages)
expect(result.cost).toBe(0)
expect(result.summary).toBe("")
@@ -697,33 +724,7 @@ describe("summarizeConversation", () => {
expect(mockApiHandler.createMessage).not.toHaveBeenCalled()
})
- it("should not summarize when there was a recent summary", async () => {
- const messages: ApiMessage[] = [
- { role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Hi there", ts: 2 },
- { role: "user", content: "How are you?", ts: 3 },
- { role: "assistant", content: "I'm good", ts: 4 },
- { role: "user", content: "What's new?", ts: 5 },
- { role: "assistant", content: "Not much", ts: 6, isSummary: true }, // Recent summary
- { role: "user", content: "Tell me more", ts: 7 },
- ]
-
- const result = await summarizeConversation(
- messages,
- mockApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- )
- expect(result.messages).toEqual(messages)
- expect(result.cost).toBe(0)
- expect(result.summary).toBe("")
- expect(result.newContextTokens).toBeUndefined()
- expect(result.error).toBeTruthy() // Error should be set for recent summary
- expect(mockApiHandler.createMessage).not.toHaveBeenCalled()
- })
-
- it("should summarize conversation and insert summary message", async () => {
+ it("should create summary with user role (fresh start model)", async () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Hello", ts: 1 },
{ role: "assistant", content: "Hi there", ts: 2 },
@@ -734,55 +735,108 @@ describe("summarizeConversation", () => {
{ role: "user", content: "Tell me more", ts: 7 },
]
- const result = await summarizeConversation(
+ const result = await summarizeConversation({
messages,
- mockApiHandler,
- defaultSystemPrompt,
+ apiHandler: mockApiHandler,
+ systemPrompt: defaultSystemPrompt,
taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- )
+ })
// Check that the API was called correctly
expect(mockApiHandler.createMessage).toHaveBeenCalled()
expect(maybeRemoveImageBlocks).toHaveBeenCalled()
- // With non-destructive condensing, the result contains ALL original messages
- // plus the summary message. Condensed messages are tagged but not deleted.
- // Use getEffectiveApiHistory to verify the effective API view matches the old behavior.
- expect(result.messages.length).toBe(messages.length + 1) // All original messages + summary
+ // Result contains all original messages (tagged) plus summary at end
+ expect(result.messages.length).toBe(messages.length + 1)
- // Check that the first message is preserved
- expect(result.messages[0]).toEqual(messages[0])
-
- // Find the summary message (it has isSummary: true)
+ // All original messages should be tagged with condenseParent
const summaryMessage = result.messages.find((m) => m.isSummary)
expect(summaryMessage).toBeDefined()
- expect(summaryMessage!.role).toBe("assistant")
- // Summary content is now always an array with [synthetic reasoning, text]
- // for DeepSeek-reasoner compatibility (requires reasoning_content on all assistant messages)
+ const condenseId = summaryMessage!.condenseId
+ expect(condenseId).toBeDefined()
+ for (const msg of result.messages.filter((m) => !m.isSummary)) {
+ expect(msg.condenseParent).toBe(condenseId)
+ }
+
+ // Summary message is a user message with just text (fresh start model)
+ expect(summaryMessage!.role).toBe("user")
expect(Array.isArray(summaryMessage!.content)).toBe(true)
const content = summaryMessage!.content as any[]
- expect(content).toHaveLength(2)
- expect(content[0].type).toBe("reasoning")
- expect(content[1].type).toBe("text")
- expect(content[1].text).toBe("This is a summary")
- expect(summaryMessage!.isSummary).toBe(true)
+ expect(content).toHaveLength(1)
+ expect(content[0].type).toBe("text")
+ expect(content[0].text).toContain("## Conversation Summary")
+ expect(content[0].text).toContain("This is a summary")
- // Verify that the effective API history matches expected: first + summary + last N messages
+ // Fresh start: effective API history should contain only the summary
const effectiveHistory = getEffectiveApiHistory(result.messages)
- expect(effectiveHistory.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // First + summary + last N
-
- // Check that condensed messages are properly tagged
- const condensedMessages = result.messages.filter((m) => m.condenseParent !== undefined)
- expect(condensedMessages.length).toBeGreaterThan(0)
+ expect(effectiveHistory).toHaveLength(1)
+ expect(effectiveHistory[0].isSummary).toBe(true)
+ expect(effectiveHistory[0].role).toBe("user")
// Check the cost and token counts
expect(result.cost).toBe(0.05)
expect(result.summary).toBe("This is a summary")
- expect(result.newContextTokens).toBe(250) // 150 output tokens + 100 from countTokens
+ // newContextTokens = countTokens(systemPrompt + summaryMessage) - counts actual content, not outputTokens
+ expect(result.newContextTokens).toBe(100) // countTokens mock returns 100
expect(result.error).toBeUndefined()
})
+ it("should preserve command blocks from first message in summary", async () => {
+ const messages: ApiMessage[] = [
+ {
+ role: "user",
+ content: 'Hello /prr #123',
+ ts: 1,
+ },
+ { role: "assistant", content: "Hi there", ts: 2 },
+ { role: "user", content: "How are you?", ts: 3 },
+ { role: "assistant", content: "I'm good", ts: 4 },
+ { role: "user", content: "What's new?", ts: 5 },
+ ]
+
+ const result = await summarizeConversation({
+ messages,
+ apiHandler: mockApiHandler,
+ systemPrompt: defaultSystemPrompt,
+ taskId,
+ })
+
+ const summaryMessage = result.messages.find((m) => m.isSummary)
+ expect(summaryMessage).toBeDefined()
+
+ const content = summaryMessage!.content as any[]
+ // Summary content is now split into separate text blocks
+ expect(content).toHaveLength(2)
+ expect(content[0].text).toContain("## Conversation Summary")
+ expect(content[1].text).toContain("")
+ expect(content[1].text).toContain("Active Workflows")
+ expect(content[1].text).toContain('')
+ })
+
+ it("should not include command blocks wrapper when no commands in first message", async () => {
+ const messages: ApiMessage[] = [
+ { role: "user", content: "Hello", ts: 1 },
+ { role: "assistant", content: "Hi there", ts: 2 },
+ { role: "user", content: "How are you?", ts: 3 },
+ { role: "assistant", content: "I'm good", ts: 4 },
+ { role: "user", content: "What's new?", ts: 5 },
+ ]
+
+ const result = await summarizeConversation({
+ messages,
+ apiHandler: mockApiHandler,
+ systemPrompt: defaultSystemPrompt,
+ taskId,
+ })
+
+ const summaryMessage = result.messages.find((m) => m.isSummary)
+ expect(summaryMessage).toBeDefined()
+
+ const content = summaryMessage!.content as any[]
+ expect(content[0].text).not.toContain("")
+ expect(content[0].text).not.toContain("Active Workflows")
+ })
+
it("should handle empty summary response and return error", async () => {
// We need enough messages to trigger summarization
const messages: ApiMessage[] = [
@@ -810,13 +864,12 @@ describe("summarizeConversation", () => {
return messages.map(({ role, content }: { role: string; content: any }) => ({ role, content }))
})
- const result = await summarizeConversation(
+ const result = await summarizeConversation({
messages,
- mockApiHandler,
- defaultSystemPrompt,
+ apiHandler: mockApiHandler,
+ systemPrompt: defaultSystemPrompt,
taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- )
+ })
// Should return original messages when summary is empty
expect(result.messages).toEqual(messages)
@@ -837,24 +890,32 @@ describe("summarizeConversation", () => {
{ role: "user", content: "Tell me more", ts: 7 },
]
- await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId, DEFAULT_PREV_CONTEXT_TOKENS)
+ await summarizeConversation({
+ messages,
+ apiHandler: mockApiHandler,
+ systemPrompt: defaultSystemPrompt,
+ taskId,
+ })
- // Verify the final request message
- const expectedFinalMessage = {
- role: "user",
- content: "Summarize the conversation so far, as described in the prompt instructions.",
- }
-
- // Verify that createMessage was called with the correct prompt
+ // Verify that createMessage was called with the SUMMARY_PROMPT (which contains CRITICAL instructions), messages array, and optional metadata
expect(mockApiHandler.createMessage).toHaveBeenCalledWith(
- expect.stringContaining("Your task is to create a detailed summary of the conversation"),
+ expect.stringContaining("You are a helpful AI assistant tasked with summarizing conversations."),
expect.any(Array),
+ undefined, // metadata is undefined when not passed to summarizeConversation
)
+ // Verify the CRITICAL instructions are included in the prompt
+ const actualPrompt = (mockApiHandler.createMessage as Mock).mock.calls[0][0]
+ expect(actualPrompt).toContain("CRITICAL: This is a summarization-only request")
+ expect(actualPrompt).toContain("CRITICAL: This summarization request is a SYSTEM OPERATION")
// Check that maybeRemoveImageBlocks was called with the correct messages
+ // The final request message now contains the detailed CONDENSE instructions
const mockCallArgs = (maybeRemoveImageBlocks as Mock).mock.calls[0][0] as any[]
- expect(mockCallArgs[mockCallArgs.length - 1]).toEqual(expectedFinalMessage)
+ const finalMessage = mockCallArgs[mockCallArgs.length - 1]
+ expect(finalMessage.role).toBe("user")
+ expect(finalMessage.content).toContain("Your task is to create a detailed summary of the conversation")
})
+
it("should include the original first user message in summarization input", async () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Initial ask", ts: 1 },
@@ -866,7 +927,12 @@ describe("summarizeConversation", () => {
{ role: "user", content: "Newest", ts: 7 },
]
- await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId, DEFAULT_PREV_CONTEXT_TOKENS)
+ await summarizeConversation({
+ messages,
+ apiHandler: mockApiHandler,
+ systemPrompt: defaultSystemPrompt,
+ taskId,
+ })
const mockCallArgs = (maybeRemoveImageBlocks as Mock).mock.calls[0][0] as any[]
@@ -904,66 +970,24 @@ describe("summarizeConversation", () => {
// Override the mock for this test
mockApiHandler.createMessage = vi.fn().mockReturnValue(streamWithUsage) as any
- const result = await summarizeConversation(
+ const result = await summarizeConversation({
messages,
- mockApiHandler,
+ apiHandler: mockApiHandler,
systemPrompt,
taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- )
+ })
- // Verify that countTokens was called with the correct messages including system prompt
+ // Verify that countTokens was called with system prompt + summary message
expect(mockApiHandler.countTokens).toHaveBeenCalled()
- // Check the newContextTokens calculation includes system prompt
- expect(result.newContextTokens).toBe(300) // 200 output tokens + 100 from countTokens
+ // newContextTokens = countTokens(systemPrompt + summaryMessage) - counts actual content
+ expect(result.newContextTokens).toBe(100) // countTokens mock returns 100
expect(result.cost).toBe(0.06)
expect(result.summary).toBe("This is a summary with system prompt")
expect(result.error).toBeUndefined()
})
- it("should return error when new context tokens >= previous context tokens", async () => {
- const messages: ApiMessage[] = [
- { role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Hi there", ts: 2 },
- { role: "user", content: "How are you?", ts: 3 },
- { role: "assistant", content: "I'm good", ts: 4 },
- { role: "user", content: "What's new?", ts: 5 },
- { role: "assistant", content: "Not much", ts: 6 },
- { role: "user", content: "Tell me more", ts: 7 },
- ]
-
- // Create a stream that produces a summary
- const streamWithLargeTokens = (async function* () {
- yield { type: "text" as const, text: "This is a very long summary that uses many tokens" }
- yield { type: "usage" as const, totalCost: 0.08, outputTokens: 500 }
- })()
-
- // Override the mock for this test
- mockApiHandler.createMessage = vi.fn().mockReturnValue(streamWithLargeTokens) as any
-
- // Mock countTokens to return a high value that when added to outputTokens (500)
- // will be >= prevContextTokens (600)
- mockApiHandler.countTokens = vi.fn().mockImplementation(() => Promise.resolve(200)) as any
-
- const prevContextTokens = 600
- const result = await summarizeConversation(
- messages,
- mockApiHandler,
- defaultSystemPrompt,
- taskId,
- prevContextTokens,
- )
-
- // Should return original messages when context would grow
- expect(result.messages).toEqual(messages)
- expect(result.cost).toBe(0.08)
- expect(result.summary).toBe("")
- expect(result.error).toBeTruthy() // Error should be set
- expect(result.newContextTokens).toBeUndefined()
- })
-
- it("should successfully summarize when new context tokens < previous context tokens", async () => {
+ it("should successfully summarize conversation", async () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Hello", ts: 1 },
{ role: "assistant", content: "Hi there", ts: 2 },
@@ -983,79 +1007,32 @@ describe("summarizeConversation", () => {
// Override the mock for this test
mockApiHandler.createMessage = vi.fn().mockReturnValue(streamWithSmallTokens) as any
- // Mock countTokens to return a small value so total is < prevContextTokens
+ // Mock countTokens to return a small value
mockApiHandler.countTokens = vi.fn().mockImplementation(() => Promise.resolve(30)) as any
- const prevContextTokens = 200
- const result = await summarizeConversation(
+ const result = await summarizeConversation({
messages,
- mockApiHandler,
- defaultSystemPrompt,
+ apiHandler: mockApiHandler,
+ systemPrompt: defaultSystemPrompt,
taskId,
- prevContextTokens,
- )
+ })
- // With non-destructive condensing, result contains all messages plus summary
- // Use getEffectiveApiHistory to verify the effective API view
- expect(result.messages.length).toBe(messages.length + 1) // All messages + summary
+ // Result contains all messages plus summary
+ expect(result.messages.length).toBe(messages.length + 1)
+
+ // Fresh start: effective history should contain only the summary
const effectiveHistory = getEffectiveApiHistory(result.messages)
- expect(effectiveHistory.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // First + summary + last N
+ expect(effectiveHistory.length).toBe(1)
+ expect(effectiveHistory[0].isSummary).toBe(true)
+
expect(result.cost).toBe(0.03)
expect(result.summary).toBe("Concise summary")
expect(result.error).toBeUndefined()
- expect(result.newContextTokens).toBe(80) // 50 output tokens + 30 from countTokens
- expect(result.newContextTokens).toBeLessThan(prevContextTokens)
+ // newContextTokens = countTokens(systemPrompt + summaryMessage) - counts actual content
+ expect(result.newContextTokens).toBe(30) // countTokens mock returns 30
})
- it("should return error when not enough messages to summarize", async () => {
- const messages: ApiMessage[] = [{ role: "user", content: "Hello", ts: 1 }]
-
- const result = await summarizeConversation(
- messages,
- mockApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- )
-
- // Should return original messages when not enough to summarize
- expect(result.messages).toEqual(messages)
- expect(result.cost).toBe(0)
- expect(result.summary).toBe("")
- expect(result.error).toBeTruthy() // Error should be set
- expect(result.newContextTokens).toBeUndefined()
- expect(mockApiHandler.createMessage).not.toHaveBeenCalled()
- })
-
- it("should return error when recent summary exists in kept messages", async () => {
- const messages: ApiMessage[] = [
- { role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Hi there", ts: 2 },
- { role: "user", content: "How are you?", ts: 3 },
- { role: "assistant", content: "I'm good", ts: 4 },
- { role: "user", content: "What's new?", ts: 5 },
- { role: "assistant", content: "Recent summary", ts: 6, isSummary: true }, // Summary in last 3 messages
- { role: "user", content: "Tell me more", ts: 7 },
- ]
-
- const result = await summarizeConversation(
- messages,
- mockApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- )
-
- // Should return original messages when recent summary exists
- expect(result.messages).toEqual(messages)
- expect(result.cost).toBe(0)
- expect(result.summary).toBe("")
- expect(result.error).toBeTruthy() // Error should be set
- expect(result.newContextTokens).toBeUndefined()
- expect(mockApiHandler.createMessage).not.toHaveBeenCalled()
- })
-
- it("should return error when both condensing and main API handlers are invalid", async () => {
+ it("should return error when API handler is invalid", async () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Hello", ts: 1 },
{ role: "assistant", content: "Hi there", ts: 2 },
@@ -1066,14 +1043,8 @@ describe("summarizeConversation", () => {
{ role: "user", content: "Tell me more", ts: 7 },
]
- // Create invalid handlers (missing createMessage)
- const invalidMainHandler = {
- countTokens: vi.fn(),
- getModel: vi.fn(),
- // createMessage is missing
- } as unknown as ApiHandler
-
- const invalidCondensingHandler = {
+ // Create invalid handler (missing createMessage)
+ const invalidHandler = {
countTokens: vi.fn(),
getModel: vi.fn(),
// createMessage is missing
@@ -1084,18 +1055,14 @@ describe("summarizeConversation", () => {
const mockError = vi.fn()
console.error = mockError
- const result = await summarizeConversation(
+ const result = await summarizeConversation({
messages,
- invalidMainHandler,
- defaultSystemPrompt,
+ apiHandler: invalidHandler,
+ systemPrompt: defaultSystemPrompt,
taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false,
- undefined,
- invalidCondensingHandler,
- )
+ })
- // Should return original messages when both handlers are invalid
+ // Should return original messages when handler is invalid
expect(result.messages).toEqual(messages)
expect(result.cost).toBe(0)
expect(result.summary).toBe("")
@@ -1103,395 +1070,66 @@ describe("summarizeConversation", () => {
expect(result.newContextTokens).toBeUndefined()
// Verify error was logged
- expect(mockError).toHaveBeenCalledWith(
- expect.stringContaining("Main API handler is also invalid for condensing"),
- )
+ expect(mockError).toHaveBeenCalledWith(expect.stringContaining("API handler is invalid for condensing"))
// Restore console.error
console.error = originalError
})
- it("should append tool_use blocks to summary message when first kept message has tool_result blocks", async () => {
- const toolUseBlock = {
- type: "tool_use" as const,
- id: "toolu_123",
- name: "read_file",
- input: { path: "test.txt" },
- }
- const toolResultBlock = {
- type: "tool_result" as const,
- tool_use_id: "toolu_123",
- content: "file contents",
- }
-
+ it("should tag all messages with condenseParent (fresh start model)", async () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Let me read that file", ts: 2 },
- { role: "user", content: "Please continue", ts: 3 },
- {
- role: "assistant",
- content: [{ type: "text" as const, text: "Reading file..." }, toolUseBlock],
- ts: 4,
- },
- {
- role: "user",
- content: [toolResultBlock, { type: "text" as const, text: "Continue" }],
- ts: 5,
- },
- { role: "assistant", content: "Got it, the file says...", ts: 6 },
- { role: "user", content: "Thanks", ts: 7 },
+ { role: "assistant", content: "Hi there", ts: 2 },
+ { role: "user", content: "How are you?", ts: 3 },
+ { role: "assistant", content: "I'm good", ts: 4 },
+ { role: "user", content: "Thanks", ts: 5 },
]
- // Create a stream with usage information
- const streamWithUsage = (async function* () {
- yield { type: "text" as const, text: "Summary of conversation" }
- yield { type: "usage" as const, totalCost: 0.05, outputTokens: 100 }
- })()
-
- mockApiHandler.createMessage = vi.fn().mockReturnValue(streamWithUsage) as any
- mockApiHandler.countTokens = vi.fn().mockImplementation(() => Promise.resolve(50)) as any
-
- const result = await summarizeConversation(
+ const result = await summarizeConversation({
messages,
- mockApiHandler,
- defaultSystemPrompt,
+ apiHandler: mockApiHandler,
+ systemPrompt: defaultSystemPrompt,
taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false, // isAutomaticTrigger
- undefined, // customCondensingPrompt
- undefined, // condensingApiHandler
- true, // useNativeTools - required for tool_use block preservation
- )
-
- // Find the summary message
- const summaryMessage = result.messages.find((m) => m.isSummary)
- expect(summaryMessage).toBeDefined()
- expect(summaryMessage!.role).toBe("assistant")
- expect(summaryMessage!.isSummary).toBe(true)
- expect(Array.isArray(summaryMessage!.content)).toBe(true)
-
- // Content should be [synthetic reasoning, text block, tool_use block]
- // The synthetic reasoning is always added for DeepSeek-reasoner compatibility
- const content = summaryMessage!.content as Anthropic.Messages.ContentBlockParam[]
- expect(content).toHaveLength(3)
- expect((content[0] as any).type).toBe("reasoning") // Synthetic reasoning for DeepSeek
- expect(content[1].type).toBe("text")
- expect((content[1] as Anthropic.Messages.TextBlockParam).text).toBe("Summary of conversation")
- expect(content[2].type).toBe("tool_use")
- expect((content[2] as Anthropic.Messages.ToolUseBlockParam).id).toBe("toolu_123")
- expect((content[2] as Anthropic.Messages.ToolUseBlockParam).name).toBe("read_file")
-
- // With non-destructive condensing, all messages are retained plus the summary
- expect(result.messages.length).toBe(messages.length + 1) // all original + summary
- // Verify effective history matches expected
- const effectiveHistory = getEffectiveApiHistory(result.messages)
- expect(effectiveHistory.length).toBe(1 + 1 + N_MESSAGES_TO_KEEP) // first + summary + last 3
- expect(result.error).toBeUndefined()
- })
-
- it("should include user tool_result message in summarize request when preserving tool_use blocks", async () => {
- const toolUseBlock = {
- type: "tool_use" as const,
- id: "toolu_history_fix",
- name: "read_file",
- input: { path: "sample.txt" },
- }
- const toolResultBlock = {
- type: "tool_result" as const,
- tool_use_id: "toolu_history_fix",
- content: "file contents",
- }
-
- const messages: ApiMessage[] = [
- { role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Let me help", ts: 2 },
- {
- role: "assistant",
- content: [{ type: "text" as const, text: "Running tool..." }, toolUseBlock],
- ts: 3,
- },
- {
- role: "user",
- content: [toolResultBlock, { type: "text" as const, text: "Thanks" }],
- ts: 4,
- },
- { role: "assistant", content: "Anything else?", ts: 5 },
- { role: "user", content: "Nope", ts: 6 },
- ]
-
- let capturedRequestMessages: any[] | undefined
- const customStream = (async function* () {
- yield { type: "text" as const, text: "Summary of conversation" }
- yield { type: "usage" as const, totalCost: 0.05, outputTokens: 100 }
- })()
-
- mockApiHandler.createMessage = vi.fn().mockImplementation((_prompt, requestMessagesParam) => {
- capturedRequestMessages = requestMessagesParam
- return customStream
- }) as any
-
- const result = await summarizeConversation(
- messages,
- mockApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false,
- undefined,
- undefined,
- true,
- )
-
- expect(result.error).toBeUndefined()
- expect(capturedRequestMessages).toBeDefined()
-
- const requestMessages = capturedRequestMessages!
- expect(requestMessages[requestMessages.length - 1]).toEqual({
- role: "user",
- content: "Summarize the conversation so far, as described in the prompt instructions.",
})
- const historyMessages = requestMessages.slice(0, -1)
- expect(historyMessages.length).toBeGreaterThanOrEqual(2)
-
- const assistantMessage = historyMessages[historyMessages.length - 2]
- const userMessage = historyMessages[historyMessages.length - 1]
-
- expect(assistantMessage.role).toBe("assistant")
- expect(Array.isArray(assistantMessage.content)).toBe(true)
- expect(
- (assistantMessage.content as any[]).some(
- (block) => block.type === "tool_use" && block.id === toolUseBlock.id,
- ),
- ).toBe(true)
-
- expect(userMessage.role).toBe("user")
- expect(Array.isArray(userMessage.content)).toBe(true)
- expect(
- (userMessage.content as any[]).some(
- (block) => block.type === "tool_result" && block.tool_use_id === toolUseBlock.id,
- ),
- ).toBe(true)
- })
-
- it("should append multiple tool_use blocks for parallel tool calls", async () => {
- const toolUseBlockA = {
- type: "tool_use" as const,
- id: "toolu_parallel_1",
- name: "search",
- input: { query: "foo" },
- }
- const toolUseBlockB = {
- type: "tool_use" as const,
- id: "toolu_parallel_2",
- name: "search",
- input: { query: "bar" },
- }
-
- const messages: ApiMessage[] = [
- { role: "user", content: "Start", ts: 1 },
- { role: "assistant", content: "Working...", ts: 2 },
- {
- role: "assistant",
- content: [{ type: "text" as const, text: "Launching parallel tools" }, toolUseBlockA, toolUseBlockB],
- ts: 3,
- },
- {
- role: "user",
- content: [
- { type: "tool_result" as const, tool_use_id: "toolu_parallel_1", content: "result A" },
- { type: "tool_result" as const, tool_use_id: "toolu_parallel_2", content: "result B" },
- { type: "text" as const, text: "Continue" },
- ],
- ts: 4,
- },
- { role: "assistant", content: "Processing results", ts: 5 },
- { role: "user", content: "Thanks", ts: 6 },
- ]
-
- const result = await summarizeConversation(
- messages,
- mockApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false,
- undefined,
- undefined,
- true,
- )
-
- // Find the summary message (it has isSummary: true)
const summaryMessage = result.messages.find((m) => m.isSummary)
expect(summaryMessage).toBeDefined()
- expect(Array.isArray(summaryMessage!.content)).toBe(true)
- const summaryContent = summaryMessage!.content as Anthropic.Messages.ContentBlockParam[]
- // First block is synthetic reasoning for DeepSeek-reasoner compatibility
- expect((summaryContent[0] as any).type).toBe("reasoning")
- // Second block is the text summary
- expect(summaryContent[1]).toEqual({ type: "text", text: "This is a summary" })
+ const condenseId = summaryMessage!.condenseId
- const preservedToolUses = summaryContent.filter(
- (block): block is Anthropic.Messages.ToolUseBlockParam => block.type === "tool_use",
- )
- expect(preservedToolUses).toHaveLength(2)
- expect(preservedToolUses.map((block) => block.id)).toEqual(["toolu_parallel_1", "toolu_parallel_2"])
+ // ALL original messages should be tagged (fresh start model tags everything)
+ for (const msg of result.messages.filter((m) => !m.isSummary)) {
+ expect(msg.condenseParent).toBe(condenseId)
+ }
})
- it("should preserve reasoning blocks in summary message for DeepSeek/Z.ai interleaved thinking", async () => {
- const reasoningBlock = {
- type: "reasoning" as const,
- text: "Let me think about this step by step...",
- }
- const toolUseBlock = {
- type: "tool_use" as const,
- id: "toolu_deepseek_reason",
- name: "read_file",
- input: { path: "test.txt" },
- }
- const toolResultBlock = {
- type: "tool_result" as const,
- tool_use_id: "toolu_deepseek_reason",
- content: "file contents",
- }
-
+ it("should place summary message at end of messages array", async () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Let me help", ts: 2 },
- { role: "user", content: "Please read the file", ts: 3 },
- {
- role: "assistant",
- // DeepSeek stores reasoning as content blocks alongside tool_use
- content: [reasoningBlock as any, { type: "text" as const, text: "Reading file..." }, toolUseBlock],
- ts: 4,
- },
- {
- role: "user",
- content: [toolResultBlock, { type: "text" as const, text: "Continue" }],
- ts: 5,
- },
- { role: "assistant", content: "Got it, the file says...", ts: 6 },
- { role: "user", content: "Thanks", ts: 7 },
+ { role: "assistant", content: "Hi there", ts: 2 },
+ { role: "user", content: "How are you?", ts: 3 },
+ { role: "assistant", content: "I'm good", ts: 4 },
+ { role: "user", content: "Thanks", ts: 5 },
]
- // Create a stream with usage information
- const streamWithUsage = (async function* () {
- yield { type: "text" as const, text: "Summary of conversation" }
- yield { type: "usage" as const, totalCost: 0.05, outputTokens: 100 }
- })()
-
- mockApiHandler.createMessage = vi.fn().mockReturnValue(streamWithUsage) as any
- mockApiHandler.countTokens = vi.fn().mockImplementation(() => Promise.resolve(50)) as any
-
- const result = await summarizeConversation(
+ const result = await summarizeConversation({
messages,
- mockApiHandler,
- defaultSystemPrompt,
+ apiHandler: mockApiHandler,
+ systemPrompt: defaultSystemPrompt,
taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false, // isAutomaticTrigger
- undefined, // customCondensingPrompt
- undefined, // condensingApiHandler
- true, // useNativeTools - required for tool_use block preservation
- )
+ })
- // Find the summary message
- const summaryMessage = result.messages.find((m) => m.isSummary)
- expect(summaryMessage).toBeDefined()
- expect(summaryMessage!.role).toBe("assistant")
- expect(summaryMessage!.isSummary).toBe(true)
- expect(Array.isArray(summaryMessage!.content)).toBe(true)
-
- // Content should be [synthetic reasoning, preserved reasoning, text block, tool_use block]
- // - Synthetic reasoning is always added for DeepSeek-reasoner compatibility
- // - Preserved reasoning from the condensed assistant message
- // This order ensures reasoning_content is always present for DeepSeek/Z.ai
- const content = summaryMessage!.content as Anthropic.Messages.ContentBlockParam[]
- expect(content).toHaveLength(4)
-
- // First block should be synthetic reasoning
- expect((content[0] as any).type).toBe("reasoning")
- expect((content[0] as any).text).toContain("Condensing conversation context")
-
- // Second block should be preserved reasoning from the condensed message
- expect((content[1] as any).type).toBe("reasoning")
- expect((content[1] as any).text).toBe("Let me think about this step by step...")
-
- // Third block should be text (the summary)
- expect(content[2].type).toBe("text")
- expect((content[2] as Anthropic.Messages.TextBlockParam).text).toBe("Summary of conversation")
-
- // Fourth block should be tool_use
- expect(content[3].type).toBe("tool_use")
- expect((content[3] as Anthropic.Messages.ToolUseBlockParam).id).toBe("toolu_deepseek_reason")
-
- expect(result.error).toBeUndefined()
- })
-
- it("should include synthetic reasoning block in summary for DeepSeek-reasoner compatibility even without tool_use blocks", async () => {
- // This test verifies the fix for the DeepSeek-reasoner 400 error:
- // "Missing `reasoning_content` field in the assistant message at message index 1"
- // DeepSeek-reasoner requires reasoning_content on ALL assistant messages, not just those with tool_calls.
- // After condensation, the summary becomes an assistant message that needs reasoning_content.
- const messages: ApiMessage[] = [
- { role: "user", content: "Tell me a joke", ts: 1 },
- { role: "assistant", content: "Why did the programmer quit?", ts: 2 },
- { role: "user", content: "I don't know, why?", ts: 3 },
- { role: "assistant", content: "He didn't get arrays!", ts: 4 },
- { role: "user", content: "Another one please", ts: 5 },
- { role: "assistant", content: "Why do programmers prefer dark mode?", ts: 6 },
- { role: "user", content: "Why?", ts: 7 },
- ]
-
- // Create a stream with usage information (no tool calls in this conversation)
- const streamWithUsage = (async function* () {
- yield { type: "text" as const, text: "Summary: User requested jokes." }
- yield { type: "usage" as const, totalCost: 0.05, outputTokens: 100 }
- })()
-
- mockApiHandler.createMessage = vi.fn().mockReturnValue(streamWithUsage) as any
- mockApiHandler.countTokens = vi.fn().mockImplementation(() => Promise.resolve(50)) as any
-
- const result = await summarizeConversation(
- messages,
- mockApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false, // isAutomaticTrigger
- undefined, // customCondensingPrompt
- undefined, // condensingApiHandler
- false, // useNativeTools - not using tools in this test
- )
-
- // Find the summary message
- const summaryMessage = result.messages.find((m) => m.isSummary)
- expect(summaryMessage).toBeDefined()
- expect(summaryMessage!.role).toBe("assistant")
- expect(summaryMessage!.isSummary).toBe(true)
-
- // CRITICAL: Content must be an array with a synthetic reasoning block
- // This is required for DeepSeek-reasoner which needs reasoning_content on all assistant messages
- expect(Array.isArray(summaryMessage!.content)).toBe(true)
- const content = summaryMessage!.content as any[]
-
- // Should have [synthetic reasoning, text]
- expect(content).toHaveLength(2)
- expect(content[0].type).toBe("reasoning")
- expect(content[0].text).toContain("Condensing conversation context")
- expect(content[1].type).toBe("text")
- expect(content[1].text).toBe("Summary: User requested jokes.")
-
- expect(result.error).toBeUndefined()
+ // Summary should be the last message
+ const lastMessage = result.messages[result.messages.length - 1]
+ expect(lastMessage.isSummary).toBe(true)
+ expect(lastMessage.role).toBe("user")
})
})
describe("summarizeConversation with custom settings", () => {
// Mock necessary dependencies
let mockMainApiHandler: ApiHandler
- let mockCondensingApiHandler: ApiHandler
const defaultSystemPrompt = "Default prompt"
- const taskId = "test-task"
+ const localTaskId = "test-task"
// Sample messages for testing
const sampleMessages: ApiMessage[] = [
@@ -1511,7 +1149,7 @@ describe("summarizeConversation with custom settings", () => {
// Reset telemetry mock
;(TelemetryService.instance.captureContextCondensed as Mock).mockClear()
- // Setup mock API handlers
+ // Setup mock API handler
mockMainApiHandler = {
createMessage: vi.fn().mockImplementation(() => {
return (async function* () {
@@ -1534,29 +1172,6 @@ describe("summarizeConversation with custom settings", () => {
},
}),
} as unknown as ApiHandler
-
- mockCondensingApiHandler = {
- createMessage: vi.fn().mockImplementation(() => {
- return (async function* () {
- yield { type: "text" as const, text: "Summary from condensing handler" }
- yield { type: "usage" as const, totalCost: 0.03, outputTokens: 80 }
- })()
- }),
- countTokens: vi.fn().mockImplementation(() => Promise.resolve(40)),
- getModel: vi.fn().mockReturnValue({
- id: "condensing-model",
- info: {
- contextWindow: 4000,
- supportsImages: true,
- supportsVision: false,
- maxTokens: 2000,
- supportsPromptCache: false,
- maxCachePoints: 0,
- minTokensPerCachePoint: 0,
- cachableFields: [],
- },
- }),
- } as unknown as ApiHandler
})
/**
@@ -1565,20 +1180,23 @@ describe("summarizeConversation with custom settings", () => {
it("should use custom prompt when provided", async () => {
const customPrompt = "Custom summarization prompt"
- await summarizeConversation(
- sampleMessages,
- mockMainApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false,
- customPrompt,
- )
+ await summarizeConversation({
+ messages: sampleMessages,
+ apiHandler: mockMainApiHandler,
+ systemPrompt: defaultSystemPrompt,
+ taskId: localTaskId,
+ isAutomaticTrigger: false,
+ customCondensingPrompt: customPrompt,
+ })
- // Verify the custom prompt was used
+ // Verify the custom prompt was used in the user message content
const createMessageCalls = (mockMainApiHandler.createMessage as Mock).mock.calls
expect(createMessageCalls.length).toBe(1)
- expect(createMessageCalls[0][0]).toBe(customPrompt)
+ // The custom prompt should be in the last message (the finalRequestMessage)
+ const requestMessages = createMessageCalls[0][1]
+ const lastMessage = requestMessages[requestMessages.length - 1]
+ expect(lastMessage.role).toBe("user")
+ expect(lastMessage.content).toBe(customPrompt)
})
/**
@@ -1586,185 +1204,81 @@ describe("summarizeConversation with custom settings", () => {
*/
it("should use default systemPrompt when custom prompt is empty or not provided", async () => {
// Test with empty string
- await summarizeConversation(
- sampleMessages,
- mockMainApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false,
- " ", // Empty custom prompt
- )
+ await summarizeConversation({
+ messages: sampleMessages,
+ apiHandler: mockMainApiHandler,
+ systemPrompt: defaultSystemPrompt,
+ taskId: localTaskId,
+ isAutomaticTrigger: false,
+ customCondensingPrompt: " ",
+ })
- // Verify the default prompt was used
+ // Verify the default SUMMARY_PROMPT was used (contains CRITICAL instructions)
let createMessageCalls = (mockMainApiHandler.createMessage as Mock).mock.calls
expect(createMessageCalls.length).toBe(1)
- expect(createMessageCalls[0][0]).toContain("Your task is to create a detailed summary")
+ expect(createMessageCalls[0][0]).toContain(
+ "You are a helpful AI assistant tasked with summarizing conversations.",
+ )
+ expect(createMessageCalls[0][0]).toContain("CRITICAL: This is a summarization-only request")
// Reset mock and test with undefined
vi.clearAllMocks()
- await summarizeConversation(
- sampleMessages,
- mockMainApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false,
- undefined, // No custom prompt
- )
+ await summarizeConversation({
+ messages: sampleMessages,
+ apiHandler: mockMainApiHandler,
+ systemPrompt: defaultSystemPrompt,
+ taskId: localTaskId,
+ isAutomaticTrigger: false,
+ })
- // Verify the default prompt was used again
+ // Verify the default SUMMARY_PROMPT was used again (contains CRITICAL instructions)
createMessageCalls = (mockMainApiHandler.createMessage as Mock).mock.calls
expect(createMessageCalls.length).toBe(1)
- expect(createMessageCalls[0][0]).toContain("Your task is to create a detailed summary")
- })
-
- /**
- * Test that condensing API handler is used when provided and valid
- */
- it("should use condensingApiHandler when provided and valid", async () => {
- await summarizeConversation(
- sampleMessages,
- mockMainApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false,
- undefined,
- mockCondensingApiHandler,
+ expect(createMessageCalls[0][0]).toContain(
+ "You are a helpful AI assistant tasked with summarizing conversations.",
)
-
- // Verify the condensing handler was used
- expect((mockCondensingApiHandler.createMessage as Mock).mock.calls.length).toBe(1)
- expect((mockMainApiHandler.createMessage as Mock).mock.calls.length).toBe(0)
- })
-
- /**
- * Test fallback to main API handler when condensing handler is not provided
- */
- it("should fall back to mainApiHandler if condensingApiHandler is not provided", async () => {
- await summarizeConversation(
- sampleMessages,
- mockMainApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false,
- undefined,
- undefined,
- )
-
- // Verify the main handler was used
- expect((mockMainApiHandler.createMessage as Mock).mock.calls.length).toBe(1)
- })
-
- /**
- * Test fallback to main API handler when condensing handler is invalid
- */
- it("should fall back to mainApiHandler if condensingApiHandler is invalid", async () => {
- // Create an invalid handler (missing createMessage)
- const invalidHandler = {
- countTokens: vi.fn(),
- getModel: vi.fn(),
- // createMessage is missing
- } as unknown as ApiHandler
-
- // Mock console.warn to verify warning message
- const originalWarn = console.warn
- const mockWarn = vi.fn()
- console.warn = mockWarn
-
- await summarizeConversation(
- sampleMessages,
- mockMainApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false,
- undefined,
- invalidHandler,
- )
-
- // Verify the main handler was used as fallback
- expect((mockMainApiHandler.createMessage as Mock).mock.calls.length).toBe(1)
-
- // Verify warning was logged
- expect(mockWarn).toHaveBeenCalledWith(
- expect.stringContaining("Chosen API handler for condensing does not support message creation"),
- )
-
- // Restore console.warn
- console.warn = originalWarn
+ expect(createMessageCalls[0][0]).toContain("CRITICAL: This is a summarization-only request")
})
/**
* Test that telemetry is called for custom prompt usage
*/
it("should capture telemetry when using custom prompt", async () => {
- await summarizeConversation(
- sampleMessages,
- mockMainApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false,
- "Custom prompt",
- )
+ await summarizeConversation({
+ messages: sampleMessages,
+ apiHandler: mockMainApiHandler,
+ systemPrompt: defaultSystemPrompt,
+ taskId: localTaskId,
+ isAutomaticTrigger: false,
+ customCondensingPrompt: "Custom prompt",
+ })
// Verify telemetry was called with custom prompt flag
expect(TelemetryService.instance.captureContextCondensed).toHaveBeenCalledWith(
- taskId,
+ localTaskId,
false,
true, // usedCustomPrompt
- false, // usedCustomApiHandler
)
})
/**
- * Test that telemetry is called for custom API handler usage
+ * Test that telemetry is called with isAutomaticTrigger flag
*/
- it("should capture telemetry when using custom API handler", async () => {
- await summarizeConversation(
- sampleMessages,
- mockMainApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false,
- undefined,
- mockCondensingApiHandler,
- )
+ it("should capture telemetry with isAutomaticTrigger flag", async () => {
+ await summarizeConversation({
+ messages: sampleMessages,
+ apiHandler: mockMainApiHandler,
+ systemPrompt: defaultSystemPrompt,
+ taskId: localTaskId,
+ isAutomaticTrigger: true,
+ customCondensingPrompt: "Custom prompt",
+ })
- // Verify telemetry was called with custom API handler flag
+ // Verify telemetry was called with isAutomaticTrigger flag
expect(TelemetryService.instance.captureContextCondensed).toHaveBeenCalledWith(
- taskId,
- false,
- false, // usedCustomPrompt
- true, // usedCustomApiHandler
- )
- })
-
- /**
- * Test that telemetry is called with both custom prompt and API handler
- */
- it("should capture telemetry when using both custom prompt and API handler", async () => {
- await summarizeConversation(
- sampleMessages,
- mockMainApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- true, // isAutomaticTrigger
- "Custom prompt",
- mockCondensingApiHandler,
- )
-
- // Verify telemetry was called with both flags
- expect(TelemetryService.instance.captureContextCondensed).toHaveBeenCalledWith(
- taskId,
+ localTaskId,
true, // isAutomaticTrigger
true, // usedCustomPrompt
- true, // usedCustomApiHandler
)
})
})
diff --git a/src/core/condense/__tests__/nested-condense.spec.ts b/src/core/condense/__tests__/nested-condense.spec.ts
new file mode 100644
index 0000000000..3868a22262
--- /dev/null
+++ b/src/core/condense/__tests__/nested-condense.spec.ts
@@ -0,0 +1,211 @@
+import { describe, it, expect } from "vitest"
+import { ApiMessage } from "../../task-persistence/apiMessages"
+import { getEffectiveApiHistory, getMessagesSinceLastSummary } from "../index"
+
+describe("nested condensing scenarios", () => {
+ describe("fresh-start model (user-role summaries)", () => {
+ it("should return only the latest summary and messages after it", () => {
+ const condenseId1 = "condense-1"
+ const condenseId2 = "condense-2"
+
+ // Simulate history after two nested condenses with user-role summaries
+ const history: ApiMessage[] = [
+ // Original task - condensed in first condense
+ { role: "user", content: "Build an app", ts: 100, condenseParent: condenseId1 },
+ // Messages from first condense
+ { role: "assistant", content: "Starting...", ts: 200, condenseParent: condenseId1 },
+ { role: "user", content: "Add auth", ts: 300, condenseParent: condenseId1 },
+ // First summary (user role, fresh-start model) - then condensed in second condense
+ {
+ role: "user",
+ content: [{ type: "text", text: "## Summary 1" }],
+ ts: 399,
+ isSummary: true,
+ condenseId: condenseId1,
+ condenseParent: condenseId2, // Tagged during second condense
+ },
+ // Messages after first condense but before second
+ { role: "assistant", content: "Auth added", ts: 400, condenseParent: condenseId2 },
+ { role: "user", content: "Add database", ts: 500, condenseParent: condenseId2 },
+ // Second summary (user role, fresh-start model)
+ {
+ role: "user",
+ content: [{ type: "text", text: "## Summary 2" }],
+ ts: 599,
+ isSummary: true,
+ condenseId: condenseId2,
+ },
+ // Messages after second condense (kept messages)
+ { role: "assistant", content: "Database added", ts: 600 },
+ { role: "user", content: "Now test it", ts: 700 },
+ ]
+
+ // Step 1: Get effective history
+ const effectiveHistory = getEffectiveApiHistory(history)
+
+ // Should only contain: Summary2, and messages after it
+ expect(effectiveHistory.length).toBe(3)
+ expect(effectiveHistory[0].isSummary).toBe(true)
+ expect(effectiveHistory[0].condenseId).toBe(condenseId2) // Latest summary
+ expect(effectiveHistory[1].content).toBe("Database added")
+ expect(effectiveHistory[2].content).toBe("Now test it")
+
+ // Verify NO condensed messages are included
+ const hasCondensedMessages = effectiveHistory.some(
+ (msg) => msg.condenseParent && history.some((m) => m.isSummary && m.condenseId === msg.condenseParent),
+ )
+ expect(hasCondensedMessages).toBe(false)
+
+ // Step 2: Get messages since last summary (on effective history)
+ const messagesSinceLastSummary = getMessagesSinceLastSummary(effectiveHistory)
+
+ // Should be the same as effective history since Summary2 is already at the start
+ expect(messagesSinceLastSummary.length).toBe(3)
+ expect(messagesSinceLastSummary[0].isSummary).toBe(true)
+ expect(messagesSinceLastSummary[0].condenseId).toBe(condenseId2)
+
+ // CRITICAL: No previous history (Summary1 or original task) should be included
+ const hasSummary1 = messagesSinceLastSummary.some((m) => m.condenseId === condenseId1)
+ expect(hasSummary1).toBe(false)
+
+ const hasOriginalTask = messagesSinceLastSummary.some((m) => m.content === "Build an app")
+ expect(hasOriginalTask).toBe(false)
+ })
+
+ it("should handle triple nested condense correctly", () => {
+ const condenseId1 = "condense-1"
+ const condenseId2 = "condense-2"
+ const condenseId3 = "condense-3"
+
+ const history: ApiMessage[] = [
+ // First condense content
+ { role: "user", content: "Task", ts: 100, condenseParent: condenseId1 },
+ {
+ role: "user",
+ content: [{ type: "text", text: "## Summary 1" }],
+ ts: 199,
+ isSummary: true,
+ condenseId: condenseId1,
+ condenseParent: condenseId2,
+ },
+ // Second condense content
+ { role: "assistant", content: "After S1", ts: 200, condenseParent: condenseId2 },
+ {
+ role: "user",
+ content: [{ type: "text", text: "## Summary 2" }],
+ ts: 299,
+ isSummary: true,
+ condenseId: condenseId2,
+ condenseParent: condenseId3,
+ },
+ // Third condense content
+ { role: "assistant", content: "After S2", ts: 300, condenseParent: condenseId3 },
+ {
+ role: "user",
+ content: [{ type: "text", text: "## Summary 3" }],
+ ts: 399,
+ isSummary: true,
+ condenseId: condenseId3,
+ },
+ // Current messages
+ { role: "assistant", content: "Current work", ts: 400 },
+ ]
+
+ const effectiveHistory = getEffectiveApiHistory(history)
+
+ // Should only contain Summary3 and current work
+ expect(effectiveHistory.length).toBe(2)
+ expect(effectiveHistory[0].condenseId).toBe(condenseId3)
+ expect(effectiveHistory[1].content).toBe("Current work")
+
+ const messagesSinceLastSummary = getMessagesSinceLastSummary(effectiveHistory)
+ expect(messagesSinceLastSummary.length).toBe(2)
+
+ // No previous summaries should be included
+ const hasPreviousSummaries = messagesSinceLastSummary.some(
+ (m) => m.condenseId === condenseId1 || m.condenseId === condenseId2,
+ )
+ expect(hasPreviousSummaries).toBe(false)
+ })
+ })
+
+ describe("getMessagesSinceLastSummary behavior with full vs effective history", () => {
+ it("should return consistent results when called with full history vs effective history", () => {
+ const condenseId = "condense-1"
+
+ const fullHistory: ApiMessage[] = [
+ { role: "user", content: "Original task", ts: 100, condenseParent: condenseId },
+ { role: "assistant", content: "Response", ts: 200, condenseParent: condenseId },
+ {
+ role: "user",
+ content: [{ type: "text", text: "Summary" }],
+ ts: 299,
+ isSummary: true,
+ condenseId,
+ },
+ { role: "assistant", content: "After summary", ts: 300 },
+ ]
+
+ // Called with FULL history (as in summarizeConversation)
+ const fromFullHistory = getMessagesSinceLastSummary(fullHistory)
+
+ // Called with EFFECTIVE history (as in attemptApiRequest)
+ const effectiveHistory = getEffectiveApiHistory(fullHistory)
+ const fromEffectiveHistory = getMessagesSinceLastSummary(effectiveHistory)
+
+ // Both should return the same messages when summary is user role
+ expect(fromFullHistory.length).toBe(fromEffectiveHistory.length)
+
+ // Both should start with the summary
+ expect(fromFullHistory[0].isSummary).toBe(true)
+ expect(fromEffectiveHistory[0].isSummary).toBe(true)
+ })
+
+ it("should not include condensed original task in effective history", () => {
+ const condenseId1 = "condense-1"
+ const condenseId2 = "condense-2"
+
+ // Scenario: Two nested condenses with user-role summaries
+ const fullHistory: ApiMessage[] = [
+ { role: "user", content: "Original task - should NOT appear", ts: 100, condenseParent: condenseId1 },
+ { role: "assistant", content: "Old response", ts: 200, condenseParent: condenseId1 },
+ // First summary (user role, fresh-start model), then condensed again
+ {
+ role: "user",
+ content: [{ type: "text", text: "Summary 1" }],
+ ts: 299,
+ isSummary: true,
+ condenseId: condenseId1,
+ condenseParent: condenseId2,
+ },
+ { role: "assistant", content: "After S1", ts: 300, condenseParent: condenseId2 },
+ // Second summary (user role, fresh-start model)
+ {
+ role: "user",
+ content: [{ type: "text", text: "Summary 2" }],
+ ts: 399,
+ isSummary: true,
+ condenseId: condenseId2,
+ },
+ { role: "assistant", content: "Current message", ts: 400 },
+ ]
+
+ const effectiveHistory = getEffectiveApiHistory(fullHistory)
+ expect(effectiveHistory.length).toBe(2) // Summary2 + Current message
+
+ const messagesSinceLastSummary = getMessagesSinceLastSummary(effectiveHistory)
+
+ // The original task should NOT be included
+ const hasOriginalTask = messagesSinceLastSummary.some((m) =>
+ typeof m.content === "string"
+ ? m.content.includes("Original task")
+ : JSON.stringify(m.content).includes("Original task"),
+ )
+ expect(hasOriginalTask).toBe(false)
+
+ // Summary1 should not be included (it was condensed)
+ const hasSummary1 = messagesSinceLastSummary.some((m) => m.condenseId === condenseId1)
+ expect(hasSummary1).toBe(false)
+ })
+ })
+})
diff --git a/src/core/condense/__tests__/rewind-after-condense.spec.ts b/src/core/condense/__tests__/rewind-after-condense.spec.ts
index f5f1a09380..068f49a857 100644
--- a/src/core/condense/__tests__/rewind-after-condense.spec.ts
+++ b/src/core/condense/__tests__/rewind-after-condense.spec.ts
@@ -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", () => {
@@ -83,7 +83,7 @@ describe("Rewind After Condense - Issue #8295", () => {
const messages: ApiMessage[] = [
{ role: "user", content: "First message", ts: 1 },
{ role: "assistant", content: "First response", ts: 2, condenseParent: condenseId },
- { role: "assistant", content: "Summary", ts: 3, isSummary: true, condenseId },
+ { role: "user", content: "Summary", ts: 3, isSummary: true, condenseId },
]
const cleaned = cleanupAfterTruncation(messages)
@@ -97,7 +97,7 @@ describe("Rewind After Condense - Issue #8295", () => {
const condenseId2 = "summary-2"
const messages: ApiMessage[] = [
{ role: "user", content: "Message 1", ts: 1, condenseParent: condenseId1 },
- { role: "assistant", content: "Summary 1", ts: 2, isSummary: true, condenseId: condenseId1 },
+ { role: "user", content: "Summary 1", ts: 2, isSummary: true, condenseId: condenseId1 },
{ role: "user", content: "Message 2", ts: 3, condenseParent: condenseId2 },
// Summary 2 is NOT present (was truncated)
]
@@ -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,24 +194,26 @@ 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 },
{ role: "user", content: "More", ts: 3, condenseParent: condenseId },
- { role: "assistant", content: "Summary", ts: 4, isSummary: true, condenseId },
- { role: "user", content: "After summary", ts: 5 },
+ { role: "user", content: "Summary", ts: 4, isSummary: true, condenseId },
+ { role: "assistant", 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)
@@ -260,7 +250,7 @@ describe("Rewind After Condense - Issue #8295", () => {
{ role: "assistant", content: "Response 3", ts: 600, condenseParent: condenseId },
{ role: "user", content: "Even more", ts: 700, condenseParent: condenseId },
// Summary gets ts = firstKeptTs - 1 = 999, which is unique
- { role: "assistant", content: "Summary", ts: firstKeptTs - 1, isSummary: true, condenseId },
+ { role: "user", content: "Summary", ts: firstKeptTs - 1, isSummary: true, condenseId },
// First kept message
{ role: "user", content: "First kept message", ts: firstKeptTs },
{ role: "assistant", content: "Response to first kept", ts: 1100 },
@@ -293,9 +283,9 @@ describe("Rewind After Condense - Issue #8295", () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Initial", ts: 1 },
- { role: "assistant", content: "Summary", ts: firstKeptTs - 1, isSummary: true, condenseId },
- { role: "user", content: "First kept message", ts: firstKeptTs },
- { role: "assistant", content: "Response", ts: 9 },
+ { role: "user", content: "Summary", ts: firstKeptTs - 1, isSummary: true, condenseId },
+ { role: "assistant", content: "First kept message", ts: firstKeptTs },
+ { role: "user", content: "Response", ts: 9 },
]
// Look up by first kept message's timestamp
@@ -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 },
@@ -341,7 +330,7 @@ describe("Rewind After Condense - Issue #8295", () => {
{ role: "user", content: "Now the tests", ts: 700, condenseParent: condenseId },
// Summary inserted before first kept message
{
- role: "assistant",
+ role: "user",
content: "Summary: Built API with validation, working on tests",
ts: 799, // msg8.ts - 1
isSummary: true,
@@ -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 (user)
+ // 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].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 },
@@ -409,7 +394,7 @@ describe("Rewind After Condense - Issue #8295", () => {
// First summary - now ALSO tagged with condenseId2 (from second condense)
{
- role: "assistant",
+ role: "user",
content: "Summary1: Built auth and database",
ts: 799,
isSummary: true,
@@ -431,7 +416,7 @@ describe("Rewind After Condense - Issue #8295", () => {
// Second summary - inserted before the last 3 kept messages
{
- role: "assistant",
+ role: "user",
content: "Summary2: App complete with auth, DB, API, validation, errors, logging. Now testing.",
ts: 1799, // msg18.ts - 1
isSummary: true,
@@ -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 (user) - 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].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,10 +474,10 @@ 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 },
+ { role: "user", content: "Summary text", ts: 399, isSummary: true, condenseId },
// Kept messages - should alternate properly
{ role: "assistant", content: "Response after summary", ts: 400 },
{ role: "user", content: "User message", ts: 500 },
@@ -505,27 +486,25 @@ describe("Rewind After Condense - Issue #8295", () => {
const effective = getEffectiveApiHistory(storage)
- // Verify the sequence: user, 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
+ // Verify the sequence: user(summary), assistant, user, assistant
+ // This is the fresh-start model with user-role summaries
expect(effective[0].role).toBe("user")
+ 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 },
- { role: "assistant", content: "Kept 2", ts: 400 },
- { role: "user", content: "Kept 3", ts: 500 },
+ { role: "user", content: "Summary", ts: 299, isSummary: true, condenseId },
+ { role: "assistant", content: "Kept 1", ts: 300 },
+ { role: "user", content: "Kept 2", ts: 400 },
+ { role: "assistant", content: "Kept 3", ts: 500 },
]
const effective = getEffectiveApiHistory(storage)
diff --git a/src/core/condense/foldedFileContext.ts b/src/core/condense/foldedFileContext.ts
new file mode 100644
index 0000000000..360dd7fc76
--- /dev/null
+++ b/src/core/condense/foldedFileContext.ts
@@ -0,0 +1,168 @@
+import * as path from "path"
+import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter"
+import { RooIgnoreController } from "../ignore/RooIgnoreController"
+
+/**
+ * Checks if a definitions string is actually an error message from tree-sitter
+ * rather than valid code definitions. These error strings should not be embedded
+ * in the folded file context - instead, the file should be skipped.
+ */
+function isTreeSitterErrorString(definitions: string): boolean {
+ // These are known error messages from parseSourceCodeDefinitionsForFile
+ const errorPatterns = ["This file does not exist", "do not have permission", "Unsupported file type:"]
+ return errorPatterns.some((pattern) => definitions.includes(pattern))
+}
+
+/**
+ * Result of generating folded file context.
+ */
+export interface FoldedFileContextResult {
+ /** The formatted string containing all folded file definitions (joined) */
+ content: string
+ /** Individual file sections, each in its own block */
+ sections: string[]
+ /** Number of files successfully processed */
+ filesProcessed: number
+ /** Number of files that failed or were skipped */
+ filesSkipped: number
+ /** Total character count of the folded content */
+ characterCount: number
+}
+
+/**
+ * Options for generating folded file context.
+ */
+export interface FoldedFileContextOptions {
+ /** Maximum total characters for the folded content (default: 50000) */
+ maxCharacters?: number
+ /** The current working directory for resolving relative paths */
+ cwd: string
+ /** Optional RooIgnoreController for file access validation */
+ rooIgnoreController?: RooIgnoreController
+}
+
+/**
+ * Generates folded (signatures-only) file context for a list of files using tree-sitter.
+ *
+ * This function takes file paths that were read during a conversation and produces
+ * a condensed representation showing only function signatures, class declarations,
+ * and other important structural definitions - hiding implementation bodies.
+ *
+ * Each file is wrapped in its own `` block during context condensation,
+ * allowing the model to retain awareness of file structure without consuming excessive tokens.
+ *
+ * @param filePaths - Array of file paths to process (relative to cwd)
+ * @param options - Configuration options including cwd and max characters
+ * @returns FoldedFileContextResult with the formatted content and statistics
+ *
+ * @example
+ * ```typescript
+ * const result = await generateFoldedFileContext(
+ * ['src/utils/helpers.ts', 'src/api/client.ts'],
+ * { cwd: '/project', maxCharacters: 30000 }
+ * )
+ * // result.content contains individual blocks for each file:
+ * //
+ * // ## File Context: src/utils/helpers.ts
+ * // 1--15 | export function formatDate(...)
+ * // 17--45 | export class DateHelper {...}
+ * //
+ * //
+ * // ## File Context: src/api/client.ts
+ * // ...
+ * //
+ * ```
+ */
+export async function generateFoldedFileContext(
+ filePaths: string[],
+ options: FoldedFileContextOptions,
+): Promise {
+ const { maxCharacters = 50000, cwd, rooIgnoreController } = options
+
+ const result: FoldedFileContextResult = {
+ content: "",
+ sections: [],
+ filesProcessed: 0,
+ filesSkipped: 0,
+ characterCount: 0,
+ }
+
+ if (filePaths.length === 0) {
+ return result
+ }
+
+ const foldedSections: string[] = []
+ let currentCharCount = 0
+ const failedFiles: string[] = []
+
+ for (let i = 0; i < filePaths.length; i++) {
+ const filePath = filePaths[i]
+ // Resolve to absolute path for tree-sitter
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath)
+
+ try {
+ // Get the folded definitions using tree-sitter
+ const definitions = await parseSourceCodeDefinitionsForFile(absolutePath, rooIgnoreController)
+
+ if (!definitions || isTreeSitterErrorString(definitions)) {
+ // File type not supported, no definitions found, or error accessing file
+ result.filesSkipped++
+ continue
+ }
+
+ // Wrap each file in its own block
+ const sectionContent = `
+## File Context: ${filePath}
+${definitions}
+`
+
+ // Check if adding this file would exceed the character limit
+ if (currentCharCount + sectionContent.length > maxCharacters) {
+ // Would exceed limit - check if we can fit at least a truncated version
+ const remainingChars = maxCharacters - currentCharCount
+ if (remainingChars < 200) {
+ // Not enough room for meaningful content, stop processing all remaining files
+ result.filesSkipped += filePaths.length - i
+ break
+ }
+
+ // Truncate the definitions to fit within the system-reminder block
+ const truncatedDefinitions = definitions.substring(0, remainingChars - 100) + "\n... (truncated)"
+ const truncatedContent = `
+## File Context: ${filePath}
+${truncatedDefinitions}
+`
+ foldedSections.push(truncatedContent)
+ currentCharCount += truncatedContent.length
+ result.filesProcessed++
+
+ // Stop processing more files since we've hit the limit
+ result.filesSkipped += filePaths.length - result.filesProcessed - result.filesSkipped
+ break
+ }
+
+ foldedSections.push(sectionContent)
+ currentCharCount += sectionContent.length
+ result.filesProcessed++
+ } catch (error) {
+ // Collect failed files for batch logging to reduce noise
+ failedFiles.push(filePath)
+ result.filesSkipped++
+ }
+ }
+
+ // Log failed files as a single batch summary instead of per-file errors
+ if (failedFiles.length > 0) {
+ console.warn(
+ `Folded context generation: skipped ${failedFiles.length} file(s) due to errors: ${failedFiles.slice(0, 5).join(", ")}${failedFiles.length > 5 ? ` and ${failedFiles.length - 5} more` : ""}`,
+ )
+ }
+
+ if (foldedSections.length > 0) {
+ result.sections = foldedSections
+ result.content = foldedSections.join("\n")
+ result.characterCount = result.content.length
+ }
+
+ return result
+}
diff --git a/src/core/condense/index.ts b/src/core/condense/index.ts
index 79bc31ef9f..5a65f0a96f 100644
--- a/src/core/condense/index.ts
+++ b/src/core/condense/index.ts
@@ -4,195 +4,118 @@ 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"
+import { RooIgnoreController } from "../ignore/RooIgnoreController"
+import { generateFoldedFileContext } from "./foldedFileContext"
-/**
- * Checks if a message contains tool_result blocks.
- * For native tools protocol, 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")
-}
+export type { FoldedFileContextResult, FoldedFileContextOptions } from "./foldedFileContext"
-/**
- * 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()
-
- // 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 = `\
-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.
+const SUMMARY_PROMPT = `You are a helpful AI assistant tasked with summarizing conversations.
-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.
+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.
-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]
- - [...]
+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.`
-Output only the summary of the conversation so far, without any additional commentary or explanation.
-`
+/**
+ * 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()
+ // Find all tool_result IDs in user messages
+ const toolResultIds = new Set()
+
+ 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 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 blocks including their content
+ const commandRegex = /]*>[\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
@@ -200,138 +123,165 @@ 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
}
+export type SummarizeConversationOptions = {
+ messages: ApiMessage[]
+ apiHandler: ApiHandler
+ systemPrompt: string
+ taskId: string
+ isAutomaticTrigger?: boolean
+ customCondensingPrompt?: string
+ metadata?: ApiHandlerCreateMessageMetadata
+ environmentDetails?: string
+ filesReadByRoo?: string[]
+ cwd?: string
+ rooIgnoreController?: RooIgnoreController
+}
+
/**
- * Summarizes the conversation messages using an LLM call
+ * Summarizes the conversation messages using an LLM call.
*
- * @param {ApiMessage[]} messages - The conversation messages
- * @param {ApiHandler} apiHandler - The API handler to use for token counting.
- * @param {string} systemPrompt - The system prompt for API requests, which should be considered in the context token count
- * @param {string} taskId - The task ID for the conversation, used for telemetry
- * @param {boolean} isAutomaticTrigger - Whether the summarization is triggered automatically
- * @returns {SummarizeResponse} - The result of the summarization operation (see above)
- */
-/**
- * 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
+ * - blocks from the original task are preserved across condensings
+ * - File context (folded code definitions) can be preserved for continuity
*
- * @param {ApiMessage[]} messages - The conversation messages
- * @param {ApiHandler} apiHandler - The API handler to use for token counting (fallback if condensingApiHandler not provided)
- * @param {string} systemPrompt - The system prompt for API requests (fallback if customCondensingPrompt not provided)
- * @param {string} taskId - The task ID for the conversation, used for telemetry
- * @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 {ApiHandler} condensingApiHandler - Optional specific API handler to use for condensing
- * @param {boolean} useNativeTools - Whether native tools protocol is being used (requires tool_use/tool_result pairing)
- * @returns {SummarizeResponse} - The result of the summarization operation (see above)
+ * Environment details handling:
+ * - For AUTOMATIC condensing (isAutomaticTrigger=true): Environment details are included
+ * in the summary because the API request is already in progress and the next user
+ * message won't have fresh environment details injected.
+ * - For MANUAL condensing (isAutomaticTrigger=false): Environment details are NOT included
+ * because fresh environment details will be injected on the very next turn via
+ * getEnvironmentDetails() in recursivelyMakeClineRequests().
*/
-export async function summarizeConversation(
- messages: ApiMessage[],
- apiHandler: ApiHandler,
- systemPrompt: string,
- taskId: string,
- prevContextTokens: number,
- isAutomaticTrigger?: boolean,
- customCondensingPrompt?: string,
- condensingApiHandler?: ApiHandler,
- useNativeTools?: boolean,
-): Promise {
+export async function summarizeConversation(options: SummarizeConversationOptions): Promise {
+ const {
+ messages,
+ apiHandler,
+ systemPrompt,
+ taskId,
+ isAutomaticTrigger,
+ customCondensingPrompt,
+ metadata,
+ environmentDetails,
+ filesReadByRoo,
+ cwd,
+ rooIgnoreController,
+ } = options
TelemetryService.instance.captureContextCondensed(
taskId,
isAutomaticTrigger ?? false,
!!customCondensingPrompt?.trim(),
- !!condensingApiHandler,
)
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
- // Only preserve these blocks when using native tools protocol (XML protocol doesn't need them)
- const { keepMessages, toolUseBlocksToPreserve, reasoningBlocksToPreserve } = useNativeTools
- ? getKeepMessagesWithToolBlocks(messages, N_MESSAGES_TO_KEEP)
- : {
- keepMessages: messages.slice(-N_MESSAGES_TO_KEEP),
- toolUseBlocksToPreserve: [],
- reasoningBlocksToPreserve: [],
- }
-
- 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
- // Use condensing API handler if provided, otherwise use main API handler
- let handlerToUse = condensingApiHandler || apiHandler
-
- // Check if the chosen handler supports the required functionality
- if (!handlerToUse || typeof handlerToUse.createMessage !== "function") {
- console.warn(
- "Chosen API handler for condensing does not support message creation or is invalid, falling back to main apiHandler.",
- )
-
- handlerToUse = apiHandler // Fallback to the main, presumably valid, apiHandler
-
- // Ensure the main apiHandler itself is valid before this point or add another check.
- if (!handlerToUse || typeof handlerToUse.createMessage !== "function") {
- // This case should ideally not happen if main apiHandler is always valid.
- // Consider throwing an error or returning a specific error response.
- console.error("Main API handler is also invalid for condensing. Cannot proceed.")
- // Return an appropriate error structure for SummarizeResponse
- const error = t("common:errors.condense_handler_invalid")
- return { ...response, error }
- }
+ // Validate that the API handler supports message creation
+ if (!apiHandler || typeof apiHandler.createMessage !== "function") {
+ console.error("API handler is invalid for condensing. Cannot proceed.")
+ const error = t("common:errors.condense_handler_invalid")
+ return { ...response, error }
}
- const stream = handlerToUse.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
+ 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,
}
}
@@ -342,146 +292,148 @@ 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}` },
+ ]
+
+ // Add command blocks (active workflows) in their own system-reminder block if present
+ if (commandBlocks) {
+ summaryContent.push({
+ type: "text",
+ text: `
+## Active Workflows
+The following directives must be maintained across all future condensings:
+${commandBlocks}
+`,
+ })
}
- const textBlock: Anthropic.Messages.TextBlockParam = { type: "text", text: summary }
+ // Generate and add folded file context (smart code folding) if file paths are provided
+ // Each file gets its own block as a separate content block
+ if (filesReadByRoo && filesReadByRoo.length > 0 && cwd) {
+ try {
+ const foldedResult = await generateFoldedFileContext(filesReadByRoo, {
+ cwd,
+ rooIgnoreController,
+ })
+ if (foldedResult.sections.length > 0) {
+ for (const section of foldedResult.sections) {
+ if (section.trim()) {
+ summaryContent.push({
+ type: "text",
+ text: section,
+ })
+ }
+ }
+ }
+ } catch (error) {
+ console.error("[summarizeConversation] Failed to generate folded file context:", error)
+ // Continue without folded context - non-critical failure
+ }
+ }
- 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 environment details as a separate text block if provided AND this is an automatic trigger.
+ // For manual condensing, fresh environment details will be injected on the next turn.
+ // For automatic condensing, the API request is already in progress so we need them in the summary.
+ if (isAutomaticTrigger && environmentDetails?.trim()) {
+ summaryContent.push({
+ type: "text",
+ text: environmentDetails,
+ })
}
// 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 contain: system prompt + summary + tool definitions
const systemPromptMessage: ApiMessage = { role: "user", content: systemPrompt }
- const contextMessages = outputTokens
- ? [systemPromptMessage, ...keepMessages]
- : [systemPromptMessage, summaryMessage, ...keepMessages]
-
- const contextBlocks = contextMessages.flatMap((message) =>
+ // Count actual summaryMessage content directly instead of using outputTokens as a proxy
+ // This ensures we account for wrapper text (## Conversation Summary, , )
+ const contextBlocks = [systemPromptMessage, summaryMessage].flatMap((message) =>
typeof message.content === "string" ? [{ text: message.content, type: "text" as const }] : message.content,
)
- const newContextTokens = outputTokens + (await apiHandler.countTokens(contextBlocks))
- if (newContextTokens >= prevContextTokens) {
- const error = t("common:errors.condense_context_grew")
- return { ...response, cost, error }
+ const messageTokens = await apiHandler.countTokens(contextBlocks)
+
+ // Count tool definition tokens if tools are provided
+ let toolTokens = 0
+ if (metadata?.tools && metadata.tools.length > 0) {
+ const toolsText = JSON.stringify(metadata.tools)
+ toolTokens = await apiHandler.countTokens([{ text: toolsText, type: "text" }])
}
+
+ const newContextTokens = messageTokens + toolTokens
return { messages: newMessages, summary, cost, newContextTokens, condenseId }
}
-/* Returns the list of all messages since the last summary message, including the summary. Returns all messages if there is no summary. */
+/**
+ * Returns the list of all messages since the last summary message, including the summary.
+ * Returns all messages if there is no summary.
+ *
+ * Note: Summary messages are always created with role: "user" (fresh-start model),
+ * so the first message since the last summary is guaranteed to be a user message.
+ */
export function getMessagesSinceLastSummary(messages: ApiMessage[]): ApiMessage[] {
- let lastSummaryIndexReverse = [...messages].reverse().findIndex((message) => message.isSummary)
+ const lastSummaryIndexReverse = [...messages].reverse().findIndex((message) => message.isSummary)
if (lastSummaryIndexReverse === -1) {
return messages
}
const lastSummaryIndex = messages.length - lastSummaryIndexReverse - 1
- const messagesSinceSummary = messages.slice(lastSummaryIndex)
-
- // Bedrock requires the first message to be a user message.
- // We preserve the original first message to maintain context.
- // See https://github.com/RooCodeInc/Roo-Code/issues/4147
- if (messagesSinceSummary.length > 0 && messagesSinceSummary[0].role !== "user") {
- // Get the original first message (should always be a user message with the task)
- const originalFirstMessage = messages[0]
- if (originalFirstMessage && originalFirstMessage.role === "user") {
- // Use the original first message unchanged to maintain full context
- return [originalFirstMessage, ...messagesSinceSummary]
- } else {
- // Fallback to generic message if no original first message exists (shouldn't happen)
- const userMessage: ApiMessage = {
- role: "user",
- content: "Please continue from the following summary:",
- ts: messages[0]?.ts ? messages[0].ts - 1 : Date.now(),
- }
- return [userMessage, ...messagesSinceSummary]
- }
- }
-
- return messagesSinceSummary
+ return messages.slice(lastSummaryIndex)
}
/**
* 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.
*
@@ -492,6 +444,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()
+ 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()
+ 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()
// Collect all truncationIds of truncation markers that exist in the current history
@@ -508,7 +525,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)) {
diff --git a/src/core/config/ContextProxy.ts b/src/core/config/ContextProxy.ts
index 64baf546bd..87ce79a325 100644
--- a/src/core/config/ContextProxy.ts
+++ b/src/core/config/ContextProxy.ts
@@ -20,6 +20,7 @@ import {
import { TelemetryService } from "@roo-code/telemetry"
import { logger } from "../../utils/logging"
+import { supportPrompt } from "../../shared/support-prompt"
type GlobalStateKey = keyof GlobalState
type SecretStateKey = keyof SecretState
@@ -92,9 +93,135 @@ export class ContextProxy {
// Migration: Sanitize invalid/removed API providers
await this.migrateInvalidApiProvider()
+ // Migration: Move legacy customCondensingPrompt to customSupportPrompts
+ await this.migrateLegacyCondensingPrompt()
+
+ // Migration: Clear old default condensing prompt so users get the improved v2 default
+ await this.migrateOldDefaultCondensingPrompt()
+
this._isInitialized = true
}
+ /**
+ * Migrates the legacy customCondensingPrompt to the new customSupportPrompts structure
+ * and removes the legacy field.
+ *
+ * Note: Only true customizations are migrated. If the legacy prompt equals the default,
+ * we skip the migration to avoid pinning users to an old default if the default changes.
+ */
+ private async migrateLegacyCondensingPrompt() {
+ try {
+ const legacyPrompt = this.originalContext.globalState.get("customCondensingPrompt")
+ if (legacyPrompt) {
+ const currentSupportPrompts =
+ this.originalContext.globalState.get>("customSupportPrompts") || {}
+
+ // Only migrate if:
+ // 1. The new location doesn't already have a value
+ // 2. The legacy prompt is a true customization (not equal to the default)
+ // This prevents pinning users to an old default if the default prompt changes.
+ const isCustomized = legacyPrompt.trim() !== supportPrompt.default.CONDENSE.trim()
+ if (!currentSupportPrompts.CONDENSE && isCustomized) {
+ logger.info("Migrating customized legacy customCondensingPrompt to customSupportPrompts")
+ const updatedPrompts = { ...currentSupportPrompts, CONDENSE: legacyPrompt }
+ await this.originalContext.globalState.update("customSupportPrompts", updatedPrompts)
+ this.stateCache.customSupportPrompts = updatedPrompts
+ } else if (!isCustomized) {
+ logger.info("Skipping migration: legacy customCondensingPrompt equals the default prompt")
+ }
+
+ // Always remove the legacy field
+ await this.originalContext.globalState.update("customCondensingPrompt", undefined)
+ this.stateCache.customCondensingPrompt = undefined
+ }
+ } catch (error) {
+ logger.error(
+ `Error during customCondensingPrompt migration: ${error instanceof Error ? error.message : String(error)}`,
+ )
+ }
+ }
+
+ /**
+ * Clears the old v1 default condensing prompt from customSupportPrompts.CONDENSE if present.
+ *
+ * Before PR #10873 "Intelligent Context Condensation v2", the default condensing prompt was
+ * a simpler 6-section format. Users who had this old default saved in their settings would
+ * be stuck with it instead of getting the improved v2 default (which includes analysis tags,
+ * error tracking, all user messages, and better task continuity).
+ *
+ * This migration uses fingerprinting to detect the old v1 default - checking for key
+ * identifying phrases unique to v1 and absence of v2-specific features. This is more
+ * lenient than exact matching and handles whitespace variations.
+ */
+ private async migrateOldDefaultCondensingPrompt() {
+ try {
+ const currentSupportPrompts =
+ this.originalContext.globalState.get>("customSupportPrompts") || {}
+
+ const savedCondensePrompt = currentSupportPrompts.CONDENSE
+
+ if (savedCondensePrompt && this.isOldV1DefaultCondensePrompt(savedCondensePrompt)) {
+ logger.info(
+ "Clearing old v1 default condensing prompt from customSupportPrompts.CONDENSE - user will now get the improved v2 default",
+ )
+
+ // Remove the CONDENSE key from customSupportPrompts
+ const { CONDENSE: _, ...remainingPrompts } = currentSupportPrompts
+ const updatedPrompts = Object.keys(remainingPrompts).length > 0 ? remainingPrompts : undefined
+
+ await this.originalContext.globalState.update("customSupportPrompts", updatedPrompts)
+ this.stateCache.customSupportPrompts = updatedPrompts
+ }
+ } catch (error) {
+ logger.error(
+ `Error during old default condensing prompt migration: ${error instanceof Error ? error.message : String(error)}`,
+ )
+ }
+ }
+
+ /**
+ * Detects if a prompt is the old v1 default condensing prompt using fingerprinting.
+ * This is more lenient than exact matching - it checks for key identifying phrases
+ * unique to v1 and absence of v2-specific features.
+ *
+ * V1 characteristics:
+ * - Exactly 6 numbered sections (1-6)
+ * - Contains specific section headers like "Previous Conversation", "Current Work", etc.
+ * - Does NOT contain v2-specific features like "", "SYSTEM OPERATION", etc.
+ */
+ private isOldV1DefaultCondensePrompt(prompt: string): boolean {
+ // Key phrases unique to the v1 default (must ALL be present)
+ const v1RequiredPhrases = [
+ "Your task is to create a detailed summary of the conversation so far",
+ "1. Previous Conversation:",
+ "2. Current Work:",
+ "3. Key Technical Concepts:",
+ "4. Relevant Files and Code:",
+ "5. Problem Solving:",
+ "6. Pending Tasks and Next Steps:",
+ "Output only the summary of the conversation so far",
+ ]
+
+ // V2-specific features (if ANY are present, this is NOT v1 default)
+ const v2Features = [
+ "",
+ "SYSTEM OPERATION",
+ "Errors and fixes",
+ "All user messages",
+ "7.", // v2 has more than 6 sections
+ "8.",
+ "9.",
+ ]
+
+ // Check that all v1 required phrases are present
+ const hasAllV1Phrases = v1RequiredPhrases.every((phrase) => prompt.toLowerCase().includes(phrase.toLowerCase()))
+
+ // Check that no v2 features are present
+ const hasNoV2Features = v2Features.every((feature) => !prompt.toLowerCase().includes(feature.toLowerCase()))
+
+ return hasAllV1Phrases && hasNoV2Features
+ }
+
/**
* Migrates invalid/removed apiProvider values by clearing them from storage.
* This handles cases where a user had a provider selected that was later removed
diff --git a/src/core/config/ProviderSettingsManager.ts b/src/core/config/ProviderSettingsManager.ts
index 420ab332b2..3024540b67 100644
--- a/src/core/config/ProviderSettingsManager.ts
+++ b/src/core/config/ProviderSettingsManager.ts
@@ -43,7 +43,6 @@ export const providerProfilesSchema = z.object({
migrations: z
.object({
rateLimitSecondsMigrated: z.boolean().optional(),
- diffSettingsMigrated: z.boolean().optional(),
openAiHeadersMigrated: z.boolean().optional(),
consecutiveMistakeLimitMigrated: z.boolean().optional(),
todoListEnabledMigrated: z.boolean().optional(),
@@ -68,7 +67,6 @@ export class ProviderSettingsManager {
modeApiConfigs: this.defaultModeApiConfigs,
migrations: {
rateLimitSecondsMigrated: true, // Mark as migrated on fresh installs
- diffSettingsMigrated: true, // Mark as migrated on fresh installs
openAiHeadersMigrated: true, // Mark as migrated on fresh installs
consecutiveMistakeLimitMigrated: true, // Mark as migrated on fresh installs
todoListEnabledMigrated: true, // Mark as migrated on fresh installs
@@ -141,7 +139,6 @@ export class ProviderSettingsManager {
if (!providerProfiles.migrations) {
providerProfiles.migrations = {
rateLimitSecondsMigrated: false,
- diffSettingsMigrated: false,
openAiHeadersMigrated: false,
consecutiveMistakeLimitMigrated: false,
todoListEnabledMigrated: false,
@@ -156,12 +153,6 @@ export class ProviderSettingsManager {
isDirty = true
}
- if (!providerProfiles.migrations.diffSettingsMigrated) {
- await this.migrateDiffSettings(providerProfiles)
- providerProfiles.migrations.diffSettingsMigrated = true
- isDirty = true
- }
-
if (!providerProfiles.migrations.openAiHeadersMigrated) {
await this.migrateOpenAiHeaders(providerProfiles)
providerProfiles.migrations.openAiHeadersMigrated = true
@@ -183,7 +174,8 @@ export class ProviderSettingsManager {
if (!providerProfiles.migrations.claudeCodeLegacySettingsMigrated) {
// These keys were used by the removed local Claude Code CLI wrapper.
for (const apiConfig of Object.values(providerProfiles.apiConfigs)) {
- if (apiConfig.apiProvider !== "claude-code") continue
+ // Cast to string for comparison since "claude-code" is no longer a valid ProviderName
+ if ((apiConfig.apiProvider as string) !== "claude-code") continue
const config = apiConfig as unknown as Record
if ("claudeCodePath" in config) {
@@ -234,41 +226,6 @@ export class ProviderSettingsManager {
}
}
- private async migrateDiffSettings(providerProfiles: ProviderProfiles) {
- try {
- let diffEnabled: boolean | undefined
- let fuzzyMatchThreshold: number | undefined
-
- try {
- diffEnabled = await this.context.globalState.get("diffEnabled")
- fuzzyMatchThreshold = await this.context.globalState.get("fuzzyMatchThreshold")
- } catch (error) {
- console.error("[MigrateDiffSettings] Error getting global diff settings:", error)
- }
-
- if (diffEnabled === undefined) {
- // Failed to get the existing value, use the default.
- diffEnabled = true
- }
-
- if (fuzzyMatchThreshold === undefined) {
- // Failed to get the existing value, use the default.
- fuzzyMatchThreshold = 1.0
- }
-
- for (const [_name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
- if (apiConfig.diffEnabled === undefined) {
- apiConfig.diffEnabled = diffEnabled
- }
- if (apiConfig.fuzzyMatchThreshold === undefined) {
- apiConfig.fuzzyMatchThreshold = fuzzyMatchThreshold
- }
- }
- } catch (error) {
- console.error(`[MigrateDiffSettings] Failed to migrate diff settings:`, error)
- }
- }
-
private async migrateOpenAiHeaders(providerProfiles: ProviderProfiles) {
try {
for (const [_name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
diff --git a/src/core/config/__tests__/ContextProxy.spec.ts b/src/core/config/__tests__/ContextProxy.spec.ts
index 49e706b181..2060260c6c 100644
--- a/src/core/config/__tests__/ContextProxy.spec.ts
+++ b/src/core/config/__tests__/ContextProxy.spec.ts
@@ -70,13 +70,18 @@ describe("ContextProxy", () => {
describe("constructor", () => {
it("should initialize state cache with all global state keys", () => {
- // +1 for the migration check of old nested settings
- expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 1)
+ // +3 for the migration checks:
+ // 1. openRouterImageGenerationSettings
+ // 2. customCondensingPrompt
+ // 3. customSupportPrompts (for migrateOldDefaultCondensingPrompt)
+ expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 3)
for (const key of GLOBAL_STATE_KEYS) {
expect(mockGlobalState.get).toHaveBeenCalledWith(key)
}
- // Also check for migration call
+ // Also check for migration calls
expect(mockGlobalState.get).toHaveBeenCalledWith("openRouterImageGenerationSettings")
+ expect(mockGlobalState.get).toHaveBeenCalledWith("customCondensingPrompt")
+ expect(mockGlobalState.get).toHaveBeenCalledWith("customSupportPrompts")
})
it("should initialize secret cache with all secret keys", () => {
@@ -99,8 +104,8 @@ describe("ContextProxy", () => {
const result = proxy.getGlobalState("apiProvider")
expect(result).toBe("deepseek")
- // Original context should be called once during updateGlobalState (+1 for migration check)
- expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 1) // From initialization + migration check
+ // Original context should be called once during updateGlobalState (+3 for migration checks)
+ expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 3) // From initialization + migration checks
})
it("should handle default values correctly", async () => {
@@ -503,4 +508,123 @@ describe("ContextProxy", () => {
expect(settings.apiProvider).toBeUndefined()
})
})
+
+ describe("old default condensing prompt migration", () => {
+ // The old v1 default condensing prompt from before PR #10873
+ const OLD_V1_DEFAULT_CONDENSE_PROMPT = `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.
+
+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.
+
+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]
+ - [...]
+
+Output only the summary of the conversation so far, without any additional commentary or explanation.`
+
+ it("should clear old v1 default condensing prompt from customSupportPrompts during initialization", async () => {
+ // Reset and create a new proxy with old v1 default prompt in customSupportPrompts
+ vi.clearAllMocks()
+ mockGlobalState.get.mockImplementation((key: string) => {
+ if (key === "customSupportPrompts") {
+ return { CONDENSE: OLD_V1_DEFAULT_CONDENSE_PROMPT }
+ }
+ return undefined
+ })
+
+ const proxyWithOldDefault = new ContextProxy(mockContext)
+ await proxyWithOldDefault.initialize()
+
+ // Should have cleared the old default by updating customSupportPrompts to undefined
+ // (since CONDENSE was the only key)
+ expect(mockGlobalState.update).toHaveBeenCalledWith("customSupportPrompts", undefined)
+ })
+
+ it("should preserve other custom prompts when clearing old v1 default", async () => {
+ // Reset and create a new proxy with old v1 default plus other custom prompts
+ vi.clearAllMocks()
+ mockGlobalState.get.mockImplementation((key: string) => {
+ if (key === "customSupportPrompts") {
+ return {
+ CONDENSE: OLD_V1_DEFAULT_CONDENSE_PROMPT,
+ EXPLAIN: "Custom explain prompt",
+ }
+ }
+ return undefined
+ })
+
+ const proxyWithOldDefault = new ContextProxy(mockContext)
+ await proxyWithOldDefault.initialize()
+
+ // Should have updated customSupportPrompts to keep EXPLAIN but remove CONDENSE
+ expect(mockGlobalState.update).toHaveBeenCalledWith("customSupportPrompts", {
+ EXPLAIN: "Custom explain prompt",
+ })
+ })
+
+ it("should not clear truly customized condensing prompts", async () => {
+ // Reset and create a new proxy with a truly customized condensing prompt
+ vi.clearAllMocks()
+ const customPrompt = "My custom condensing instructions"
+ mockGlobalState.get.mockImplementation((key: string) => {
+ if (key === "customSupportPrompts") {
+ return { CONDENSE: customPrompt }
+ }
+ return undefined
+ })
+
+ const proxyWithCustomPrompt = new ContextProxy(mockContext)
+ await proxyWithCustomPrompt.initialize()
+
+ // Should NOT have called update for customSupportPrompts (custom prompt should be preserved)
+ const updateCalls = mockGlobalState.update.mock.calls
+ const customSupportPromptsUpdateCalls = updateCalls.filter(
+ (call: any[]) => call[0] === "customSupportPrompts",
+ )
+ expect(customSupportPromptsUpdateCalls.length).toBe(0)
+ })
+
+ it("should not fail when customSupportPrompts is undefined", async () => {
+ // Reset and create a new proxy with no customSupportPrompts
+ vi.clearAllMocks()
+ mockGlobalState.get.mockReturnValue(undefined)
+
+ const proxyWithNoPrompts = new ContextProxy(mockContext)
+ await proxyWithNoPrompts.initialize()
+
+ // Should not have called update for customSupportPrompts
+ const updateCalls = mockGlobalState.update.mock.calls
+ const customSupportPromptsUpdateCalls = updateCalls.filter(
+ (call: any[]) => call[0] === "customSupportPrompts",
+ )
+ expect(customSupportPromptsUpdateCalls.length).toBe(0)
+ })
+ })
})
diff --git a/src/core/config/__tests__/ProviderSettingsManager.spec.ts b/src/core/config/__tests__/ProviderSettingsManager.spec.ts
index 0669d9591c..e233fc913c 100644
--- a/src/core/config/__tests__/ProviderSettingsManager.spec.ts
+++ b/src/core/config/__tests__/ProviderSettingsManager.spec.ts
@@ -57,14 +57,11 @@ describe("ProviderSettingsManager", () => {
default: {
config: {},
id: "default",
- diffEnabled: true,
- fuzzyMatchThreshold: 1.0,
},
},
modeApiConfigs: {},
migrations: {
rateLimitSecondsMigrated: true,
- diffSettingsMigrated: true,
openAiHeadersMigrated: true,
consecutiveMistakeLimitMigrated: true,
todoListEnabledMigrated: true,
@@ -93,7 +90,6 @@ describe("ProviderSettingsManager", () => {
},
migrations: {
rateLimitSecondsMigrated: true,
- diffSettingsMigrated: true,
},
}),
)
@@ -170,7 +166,6 @@ describe("ProviderSettingsManager", () => {
},
migrations: {
rateLimitSecondsMigrated: true,
- diffSettingsMigrated: true,
openAiHeadersMigrated: true,
consecutiveMistakeLimitMigrated: false,
},
@@ -211,7 +206,6 @@ describe("ProviderSettingsManager", () => {
},
migrations: {
rateLimitSecondsMigrated: true,
- diffSettingsMigrated: true,
openAiHeadersMigrated: true,
consecutiveMistakeLimitMigrated: true,
todoListEnabledMigrated: false,
@@ -260,7 +254,6 @@ describe("ProviderSettingsManager", () => {
},
migrations: {
rateLimitSecondsMigrated: true,
- diffSettingsMigrated: true,
openAiHeadersMigrated: true,
consecutiveMistakeLimitMigrated: true,
todoListEnabledMigrated: true,
@@ -298,7 +291,6 @@ describe("ProviderSettingsManager", () => {
},
migrations: {
rateLimitSecondsMigrated: true,
- diffSettingsMigrated: true,
openAiHeadersMigrated: true,
consecutiveMistakeLimitMigrated: true,
todoListEnabledMigrated: true,
@@ -329,7 +321,6 @@ describe("ProviderSettingsManager", () => {
},
migrations: {
rateLimitSecondsMigrated: true,
- diffSettingsMigrated: true,
openAiHeadersMigrated: true,
consecutiveMistakeLimitMigrated: true,
todoListEnabledMigrated: true,
@@ -565,7 +556,6 @@ describe("ProviderSettingsManager", () => {
apiConfigs: { default: {} },
migrations: {
rateLimitSecondsMigrated: true,
- diffSettingsMigrated: true,
openAiHeadersMigrated: true,
},
}),
@@ -694,7 +684,6 @@ describe("ProviderSettingsManager", () => {
apiConfigs: { test: { apiProvider: "anthropic", id: "test-id" } },
migrations: {
rateLimitSecondsMigrated: true,
- diffSettingsMigrated: true,
openAiHeadersMigrated: true,
},
}),
@@ -727,7 +716,6 @@ describe("ProviderSettingsManager", () => {
},
migrations: {
rateLimitSecondsMigrated: true,
- diffSettingsMigrated: true,
openAiHeadersMigrated: true,
consecutiveMistakeLimitMigrated: true,
todoListEnabledMigrated: true,
diff --git a/src/core/config/__tests__/importExport.spec.ts b/src/core/config/__tests__/importExport.spec.ts
index 3d5329f377..9873ffde94 100644
--- a/src/core/config/__tests__/importExport.spec.ts
+++ b/src/core/config/__tests__/importExport.spec.ts
@@ -27,6 +27,7 @@ vi.mock("vscode", () => ({
showSaveDialog: vi.fn(),
showErrorMessage: vi.fn(),
showInformationMessage: vi.fn(),
+ showWarningMessage: vi.fn(),
},
Uri: {
file: vi.fn((filePath) => ({ fsPath: filePath })),
@@ -68,15 +69,6 @@ vi.mock("../../../api", () => ({
buildApiHandler: vi.fn().mockImplementation((config) => {
// Return different model info based on the provider and model
const getModelInfo = () => {
- if (config.apiProvider === "claude-code") {
- return {
- id: config.apiModelId || "claude-sonnet-4-5",
- info: {
- supportsReasoningBudget: false,
- requiredReasoningBudget: false,
- },
- }
- }
if (config.apiProvider === "anthropic" && config.apiModelId === "claude-3-5-sonnet-20241022") {
return {
id: "claude-3-5-sonnet-20241022",
@@ -126,6 +118,7 @@ describe("importExport", () => {
setValue: vi.fn(),
export: vi.fn().mockImplementation(() => Promise.resolve({})),
setProviderSettings: vi.fn(),
+ getValue: vi.fn(),
} as unknown as ReturnType>
mockCustomModesManager = { updateCustomMode: vi.fn() } as unknown as ReturnType<
@@ -157,6 +150,7 @@ describe("importExport", () => {
expect(vscode.window.showOpenDialog).toHaveBeenCalledWith({
filters: { JSON: ["json"] },
canSelectMany: false,
+ defaultUri: expect.anything(), // Defaults to Downloads or last export path
})
expect(fs.readFile).not.toHaveBeenCalled()
@@ -458,6 +452,7 @@ describe("importExport", () => {
const mockProvider = {
settingsImportedAt: 0,
postStateToWebview: vi.fn().mockResolvedValue(undefined),
+ postStateToWebviewWithoutTaskHistory: vi.fn().mockResolvedValue(undefined),
}
// Mock the showErrorMessage to capture the error
@@ -483,18 +478,17 @@ describe("importExport", () => {
it("should handle import when reasoning budget fields are missing from config", async () => {
// This test verifies that import works correctly when reasoning budget fields are not present
- // Using claude-code provider which doesn't support reasoning budgets
;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
const mockFileContent = JSON.stringify({
providerProfiles: {
- currentApiConfigName: "claude-code-provider",
+ currentApiConfigName: "openai-provider",
apiConfigs: {
- "claude-code-provider": {
- apiProvider: "claude-code" as ProviderName,
- apiModelId: "claude-3-5-sonnet-20241022",
- id: "claude-code-id",
+ "openai-provider": {
+ apiProvider: "openai" as ProviderName,
+ apiModelId: "gpt-4",
+ id: "openai-id",
apiKey: "test-key",
// No modelMaxTokens or modelMaxThinkingTokens fields
},
@@ -512,7 +506,7 @@ describe("importExport", () => {
mockProviderSettingsManager.export.mockResolvedValue(previousProviderProfiles)
mockProviderSettingsManager.listConfig.mockResolvedValue([
- { name: "claude-code-provider", id: "claude-code-id", apiProvider: "claude-code" as ProviderName },
+ { name: "openai-provider", id: "openai-id", apiProvider: "openai" as ProviderName },
{ name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName },
])
@@ -529,21 +523,502 @@ describe("importExport", () => {
expect(mockProviderSettingsManager.export).toHaveBeenCalled()
expect(mockProviderSettingsManager.import).toHaveBeenCalledWith({
- currentApiConfigName: "claude-code-provider",
+ currentApiConfigName: "openai-provider",
apiConfigs: {
default: { apiProvider: "anthropic" as ProviderName, id: "default-id" },
- "claude-code-provider": {
- apiProvider: "claude-code" as ProviderName,
- apiModelId: "claude-3-5-sonnet-20241022",
+ "openai-provider": {
+ apiProvider: "openai" as ProviderName,
+ apiModelId: "gpt-4",
apiKey: "test-key",
- id: "claude-code-id",
+ id: "openai-id",
},
},
modeApiConfigs: {},
})
expect(mockContextProxy.setValues).toHaveBeenCalledWith({ mode: "code", autoApprovalEnabled: true })
- expect(mockContextProxy.setValue).toHaveBeenCalledWith("currentApiConfigName", "claude-code-provider")
+ expect(mockContextProxy.setValue).toHaveBeenCalledWith("currentApiConfigName", "openai-provider")
+ })
+
+ describe("lenient import with invalid providers", () => {
+ it("should sanitize profiles with invalid apiProvider and return warnings", async () => {
+ // Test importing a profile with a removed/invalid provider like "claude-code"
+ ;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
+
+ const mockFileContent = JSON.stringify({
+ providerProfiles: {
+ currentApiConfigName: "valid-profile",
+ apiConfigs: {
+ "valid-profile": {
+ apiProvider: "openai" as ProviderName,
+ apiKey: "test-key",
+ id: "valid-id",
+ },
+ "invalid-profile": {
+ apiProvider: "claude-code", // Invalid/removed provider
+ apiKey: "some-key",
+ id: "invalid-id",
+ },
+ },
+ },
+ globalSettings: { mode: "code" },
+ })
+
+ ;(fs.readFile as Mock).mockResolvedValue(mockFileContent)
+
+ mockProviderSettingsManager.export.mockResolvedValue({
+ currentApiConfigName: "default",
+ apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
+ })
+ mockProviderSettingsManager.listConfig.mockResolvedValue([
+ { name: "valid-profile", id: "valid-id", apiProvider: "openai" as ProviderName },
+ { name: "default", id: "default-id", apiProvider: "anthropic" as ProviderName },
+ ])
+
+ const result = await importSettings({
+ providerSettingsManager: mockProviderSettingsManager,
+ contextProxy: mockContextProxy,
+ customModesManager: mockCustomModesManager,
+ })
+
+ // Import should succeed
+ expect(result.success).toBe(true)
+
+ // Should have warnings about the sanitized profile
+ expect(result).toHaveProperty("warnings")
+ expect((result as { warnings?: string[] }).warnings).toBeDefined()
+ expect((result as { warnings?: string[] }).warnings!.length).toBeGreaterThan(0)
+ expect((result as { warnings?: string[] }).warnings![0]).toContain("invalid-profile")
+ expect((result as { warnings?: string[] }).warnings![0]).toContain("claude-code")
+
+ // The valid profile should be imported
+ expect(mockProviderSettingsManager.import).toHaveBeenCalled()
+ const importedProfiles = mockProviderSettingsManager.import.mock.calls[0][0]
+ expect(importedProfiles.apiConfigs["valid-profile"]).toBeDefined()
+ expect(importedProfiles.apiConfigs["valid-profile"].apiProvider).toBe("openai")
+
+ // The invalid profile should still be imported but without apiProvider
+ expect(importedProfiles.apiConfigs["invalid-profile"]).toBeDefined()
+ expect(importedProfiles.apiConfigs["invalid-profile"].apiProvider).toBeUndefined()
+ })
+
+ it("should skip completely invalid profiles and return warnings", async () => {
+ ;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
+
+ const mockFileContent = JSON.stringify({
+ providerProfiles: {
+ currentApiConfigName: "valid-profile",
+ apiConfigs: {
+ "valid-profile": {
+ apiProvider: "openai" as ProviderName,
+ apiKey: "test-key",
+ id: "valid-id",
+ },
+ "type-invalid": {
+ // Invalid type - modelTemperature should be a number, not a string
+ modelTemperature: "not-a-number",
+ id: "type-invalid-id",
+ },
+ },
+ },
+ globalSettings: { mode: "code" },
+ })
+
+ ;(fs.readFile as Mock).mockResolvedValue(mockFileContent)
+
+ mockProviderSettingsManager.export.mockResolvedValue({
+ currentApiConfigName: "default",
+ apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
+ })
+ mockProviderSettingsManager.listConfig.mockResolvedValue([
+ { name: "valid-profile", id: "valid-id", apiProvider: "openai" as ProviderName },
+ ])
+
+ const result = await importSettings({
+ providerSettingsManager: mockProviderSettingsManager,
+ contextProxy: mockContextProxy,
+ customModesManager: mockCustomModesManager,
+ })
+
+ // Import should succeed (valid profile was imported)
+ expect(result.success).toBe(true)
+
+ // Should have warnings about the skipped profile
+ expect((result as { warnings?: string[] }).warnings).toBeDefined()
+ expect((result as { warnings?: string[] }).warnings!.some((w) => w.includes("type-invalid"))).toBe(true)
+ expect((result as { warnings?: string[] }).warnings!.some((w) => w.includes("skipped"))).toBe(true)
+
+ // The valid profile should be imported
+ const importedProfiles = mockProviderSettingsManager.import.mock.calls[0][0]
+ expect(importedProfiles.apiConfigs["valid-profile"]).toBeDefined()
+
+ // The type-invalid profile should NOT be imported
+ expect(importedProfiles.apiConfigs["type-invalid"]).toBeUndefined()
+ })
+
+ it("should fail when NO valid profiles can be imported", async () => {
+ ;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
+
+ const mockFileContent = JSON.stringify({
+ providerProfiles: {
+ currentApiConfigName: "invalid-profile",
+ apiConfigs: {
+ "invalid-profile-1": {
+ // Invalid type - rateLimitSeconds should be number
+ rateLimitSeconds: "not-a-number",
+ id: "invalid-1",
+ },
+ "invalid-profile-2": {
+ // Invalid type - modelTemperature should be number
+ modelTemperature: { invalid: "object" },
+ id: "invalid-2",
+ },
+ },
+ },
+ globalSettings: { mode: "code" },
+ })
+
+ ;(fs.readFile as Mock).mockResolvedValue(mockFileContent)
+
+ mockProviderSettingsManager.export.mockResolvedValue({
+ currentApiConfigName: "default",
+ apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
+ })
+
+ const result = await importSettings({
+ providerSettingsManager: mockProviderSettingsManager,
+ contextProxy: mockContextProxy,
+ customModesManager: mockCustomModesManager,
+ })
+
+ // Import should fail since all profiles have schema validation errors
+ expect(result.success).toBe(false)
+ expect(result.error).toContain("No valid profiles could be imported")
+
+ // Should NOT have called import since there were no valid profiles
+ expect(mockProviderSettingsManager.import).not.toHaveBeenCalled()
+ })
+
+ it("should show warning notification when importing with warnings via importSettingsWithFeedback", async () => {
+ const filePath = "/mock/path/settings.json"
+ const mockFileContent = JSON.stringify({
+ providerProfiles: {
+ currentApiConfigName: "valid-profile",
+ apiConfigs: {
+ "valid-profile": {
+ apiProvider: "openai" as ProviderName,
+ apiKey: "test-key",
+ id: "valid-id",
+ },
+ "problematic-profile": {
+ apiProvider: "removed-provider", // Invalid provider
+ apiKey: "some-key",
+ id: "problematic-id",
+ },
+ },
+ },
+ globalSettings: { mode: "code" },
+ })
+
+ ;(fs.readFile as Mock).mockResolvedValue(mockFileContent)
+ ;(fs.access as Mock).mockResolvedValue(undefined)
+
+ mockProviderSettingsManager.export.mockResolvedValue({
+ currentApiConfigName: "default",
+ apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
+ })
+ mockProviderSettingsManager.listConfig.mockResolvedValue([
+ { name: "valid-profile", id: "valid-id", apiProvider: "openai" as ProviderName },
+ ])
+
+ const mockProvider = {
+ settingsImportedAt: 0,
+ postStateToWebview: vi.fn().mockResolvedValue(undefined),
+ }
+
+ const showWarningMessageSpy = vi.spyOn(vscode.window, "showWarningMessage").mockResolvedValue(undefined)
+ const showInfoMessageSpy = vi
+ .spyOn(vscode.window, "showInformationMessage")
+ .mockResolvedValue(undefined)
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
+
+ await importSettingsWithFeedback(
+ {
+ providerSettingsManager: mockProviderSettingsManager,
+ contextProxy: mockContextProxy,
+ customModesManager: mockCustomModesManager,
+ provider: mockProvider,
+ },
+ filePath,
+ )
+
+ // Should show warning message with short summary (not full details)
+ expect(showWarningMessageSpy).toHaveBeenCalledWith(
+ expect.stringContaining("1 profile had issues during import."),
+ )
+ expect(showWarningMessageSpy).toHaveBeenCalledWith(
+ expect.stringContaining("See Developer Tools console for details."),
+ )
+ // Should log full details to console
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
+ "Settings import completed with warnings:",
+ expect.arrayContaining([expect.stringContaining("problematic-profile")]),
+ )
+ expect(showInfoMessageSpy).not.toHaveBeenCalled()
+
+ // Provider state should still be updated
+ expect(mockProvider.settingsImportedAt).toBeGreaterThan(0)
+ expect(mockProvider.postStateToWebview).toHaveBeenCalled()
+
+ showWarningMessageSpy.mockRestore()
+ showInfoMessageSpy.mockRestore()
+ consoleWarnSpy.mockRestore()
+ })
+
+ it("should handle multiple profiles with mixed valid and invalid providers", async () => {
+ ;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
+
+ const mockFileContent = JSON.stringify({
+ providerProfiles: {
+ currentApiConfigName: "anthropic-profile",
+ apiConfigs: {
+ "anthropic-profile": {
+ apiProvider: "anthropic" as ProviderName,
+ anthropicApiKey: "key-1",
+ id: "anthropic-id",
+ },
+ "openai-profile": {
+ apiProvider: "openai" as ProviderName,
+ apiKey: "key-2",
+ id: "openai-id",
+ },
+ "old-claude-profile": {
+ apiProvider: "claude-code", // Removed provider
+ apiKey: "key-3",
+ id: "claude-id",
+ },
+ "another-invalid": {
+ apiProvider: "some-old-provider", // Another removed provider
+ apiKey: "key-4",
+ id: "another-id",
+ },
+ },
+ },
+ globalSettings: { mode: "code" },
+ })
+
+ ;(fs.readFile as Mock).mockResolvedValue(mockFileContent)
+
+ mockProviderSettingsManager.export.mockResolvedValue({
+ currentApiConfigName: "default",
+ apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
+ })
+ mockProviderSettingsManager.listConfig.mockResolvedValue([
+ { name: "anthropic-profile", id: "anthropic-id", apiProvider: "anthropic" as ProviderName },
+ { name: "openai-profile", id: "openai-id", apiProvider: "openai" as ProviderName },
+ ])
+
+ const result = await importSettings({
+ providerSettingsManager: mockProviderSettingsManager,
+ contextProxy: mockContextProxy,
+ customModesManager: mockCustomModesManager,
+ })
+
+ // Import should succeed
+ expect(result.success).toBe(true)
+
+ // Should have multiple warnings
+ const warnings = (result as { warnings?: string[] }).warnings!
+ expect(warnings.length).toBe(2) // Two profiles had invalid providers
+ expect(warnings.some((w) => w.includes("old-claude-profile"))).toBe(true)
+ expect(warnings.some((w) => w.includes("another-invalid"))).toBe(true)
+
+ // Valid profiles should be imported correctly
+ const importedProfiles = mockProviderSettingsManager.import.mock.calls[0][0]
+ expect(importedProfiles.apiConfigs["anthropic-profile"].apiProvider).toBe("anthropic")
+ expect(importedProfiles.apiConfigs["openai-profile"].apiProvider).toBe("openai")
+
+ // Invalid provider profiles should have apiProvider removed
+ expect(importedProfiles.apiConfigs["old-claude-profile"].apiProvider).toBeUndefined()
+ expect(importedProfiles.apiConfigs["another-invalid"].apiProvider).toBeUndefined()
+ })
+
+ it("should fallback currentApiConfigName when the imported current profile was skipped", async () => {
+ ;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
+
+ // Import file where currentApiConfigName points to an invalid profile that gets skipped
+ const mockFileContent = JSON.stringify({
+ providerProfiles: {
+ currentApiConfigName: "invalid-current-profile", // This profile is completely invalid
+ apiConfigs: {
+ "invalid-current-profile": {
+ // Invalid type - rateLimitSeconds should be number
+ rateLimitSeconds: "not-a-number",
+ id: "invalid-current-id",
+ },
+ "valid-fallback-profile": {
+ apiProvider: "openai" as ProviderName,
+ apiKey: "test-key",
+ id: "fallback-id",
+ },
+ },
+ },
+ globalSettings: { mode: "code" },
+ })
+
+ ;(fs.readFile as Mock).mockResolvedValue(mockFileContent)
+
+ mockProviderSettingsManager.export.mockResolvedValue({
+ currentApiConfigName: "default",
+ apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
+ })
+ mockProviderSettingsManager.listConfig.mockResolvedValue([
+ { name: "valid-fallback-profile", id: "fallback-id", apiProvider: "openai" as ProviderName },
+ ])
+
+ const result = await importSettings({
+ providerSettingsManager: mockProviderSettingsManager,
+ contextProxy: mockContextProxy,
+ customModesManager: mockCustomModesManager,
+ })
+
+ // Import should succeed
+ expect(result.success).toBe(true)
+
+ // Should have warnings about the skipped profile AND the fallback
+ const warnings = (result as { warnings?: string[] }).warnings!
+ expect(warnings).toBeDefined()
+ expect(warnings.some((w) => w.includes("invalid-current-profile") && w.includes("skipped"))).toBe(true)
+ expect(
+ warnings.some(
+ (w) =>
+ w.includes("invalid-current-profile") &&
+ w.includes("not available") &&
+ w.includes("valid-fallback-profile"),
+ ),
+ ).toBe(true)
+
+ // The currentApiConfigName should be set to the valid fallback profile, not the invalid one
+ const importedProfiles = mockProviderSettingsManager.import.mock.calls[0][0]
+ expect(importedProfiles.currentApiConfigName).toBe("valid-fallback-profile")
+
+ // contextProxy should also be set with the fallback profile name
+ expect(mockContextProxy.setValue).toHaveBeenCalledWith("currentApiConfigName", "valid-fallback-profile")
+
+ // The invalid profile should NOT be imported
+ expect(importedProfiles.apiConfigs["invalid-current-profile"]).toBeUndefined()
+ // The valid fallback profile should be imported
+ expect(importedProfiles.apiConfigs["valid-fallback-profile"]).toBeDefined()
+ })
+
+ it("should keep previous currentApiConfigName when all imported profiles are invalid", async () => {
+ ;(vscode.window.showOpenDialog as Mock).mockResolvedValue([{ fsPath: "/mock/path/settings.json" }])
+
+ // All profiles in the import are invalid, but we have existing profiles
+ const mockFileContent = JSON.stringify({
+ providerProfiles: {
+ currentApiConfigName: "invalid-profile",
+ apiConfigs: {
+ "invalid-profile": {
+ rateLimitSeconds: "not-a-number",
+ id: "invalid-id",
+ },
+ },
+ },
+ globalSettings: { mode: "code" },
+ })
+
+ ;(fs.readFile as Mock).mockResolvedValue(mockFileContent)
+
+ mockProviderSettingsManager.export.mockResolvedValue({
+ currentApiConfigName: "existing-profile",
+ apiConfigs: {
+ "existing-profile": { apiProvider: "anthropic" as ProviderName, id: "existing-id" },
+ },
+ })
+
+ const result = await importSettings({
+ providerSettingsManager: mockProviderSettingsManager,
+ contextProxy: mockContextProxy,
+ customModesManager: mockCustomModesManager,
+ })
+
+ // Import should fail because no valid profiles could be imported
+ expect(result.success).toBe(false)
+ expect(result.error).toContain("No valid profiles could be imported")
+ })
+
+ it("should show plural summary for multiple profile warnings via importSettingsWithFeedback", async () => {
+ const filePath = "/mock/path/settings.json"
+ const mockFileContent = JSON.stringify({
+ providerProfiles: {
+ currentApiConfigName: "valid-profile",
+ apiConfigs: {
+ "valid-profile": {
+ apiProvider: "openai" as ProviderName,
+ apiKey: "test-key",
+ id: "valid-id",
+ },
+ "problematic-profile-1": {
+ apiProvider: "removed-provider-1",
+ apiKey: "key-1",
+ id: "problematic-id-1",
+ },
+ "problematic-profile-2": {
+ apiProvider: "removed-provider-2",
+ apiKey: "key-2",
+ id: "problematic-id-2",
+ },
+ },
+ },
+ globalSettings: { mode: "code" },
+ })
+
+ ;(fs.readFile as Mock).mockResolvedValue(mockFileContent)
+ ;(fs.access as Mock).mockResolvedValue(undefined)
+
+ mockProviderSettingsManager.export.mockResolvedValue({
+ currentApiConfigName: "default",
+ apiConfigs: { default: { apiProvider: "anthropic" as ProviderName, id: "default-id" } },
+ })
+ mockProviderSettingsManager.listConfig.mockResolvedValue([
+ { name: "valid-profile", id: "valid-id", apiProvider: "openai" as ProviderName },
+ ])
+
+ const mockProvider = {
+ settingsImportedAt: 0,
+ postStateToWebview: vi.fn().mockResolvedValue(undefined),
+ }
+
+ const showWarningMessageSpy = vi.spyOn(vscode.window, "showWarningMessage").mockResolvedValue(undefined)
+ const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {})
+
+ await importSettingsWithFeedback(
+ {
+ providerSettingsManager: mockProviderSettingsManager,
+ contextProxy: mockContextProxy,
+ customModesManager: mockCustomModesManager,
+ provider: mockProvider,
+ },
+ filePath,
+ )
+
+ // Should show warning message with plural summary for multiple warnings
+ expect(showWarningMessageSpy).toHaveBeenCalledWith(
+ expect.stringContaining("2 profiles had issues during import."),
+ )
+ // Should log full details to console
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
+ "Settings import completed with warnings:",
+ expect.arrayContaining([
+ expect.stringContaining("problematic-profile-1"),
+ expect.stringContaining("problematic-profile-2"),
+ ]),
+ )
+
+ showWarningMessageSpy.mockRestore()
+ consoleWarnSpy.mockRestore()
+ })
})
})
@@ -702,7 +1177,7 @@ describe("importExport", () => {
defaultUri: expect.anything(),
})
- expect(vscode.Uri.file).toHaveBeenCalledWith(path.join("/mock/home", "Documents", "roo-code-settings.json"))
+ expect(vscode.Uri.file).toHaveBeenCalledWith(path.join("/mock/home", "Downloads", "roo-code-settings.json"))
})
describe("codebase indexing export", () => {
@@ -1721,27 +2196,27 @@ describe("importExport", () => {
it.each([
{
testCase: "supportsReasoningBudget is false",
- providerName: "claude-code-provider",
- modelId: "claude-sonnet-4-5",
- providerId: "claude-code-id",
+ providerName: "deepseek-provider",
+ modelId: "deepseek-chat",
+ providerId: "deepseek-id",
},
{
testCase: "requiredReasoningBudget is false",
- providerName: "claude-code-provider-2",
- modelId: "claude-sonnet-4-5",
- providerId: "claude-code-id-2",
+ providerName: "deepseek-provider-2",
+ modelId: "deepseek-coder",
+ providerId: "deepseek-id-2",
},
{
testCase: "both supportsReasoningBudget and requiredReasoningBudget are false",
- providerName: "claude-code-provider-3",
- modelId: "claude-3-5-haiku-20241022",
- providerId: "claude-code-id-3",
+ providerName: "deepseek-provider-3",
+ modelId: "deepseek-reasoner",
+ providerId: "deepseek-id-3",
},
])(
"should exclude modelMaxTokens and modelMaxThinkingTokens when $testCase",
async ({ providerName, modelId, providerId }) => {
// This test verifies that token fields are excluded when model doesn't support reasoning budget
- // Using claude-code provider which has supportsReasoningBudget: false and requiredReasoningBudget: false
+ // Using deepseek provider which uses apiModelId and has supportsReasoningBudget: false
;(vscode.window.showSaveDialog as Mock).mockResolvedValue({
fsPath: "/mock/path/roo-code-settings.json",
@@ -1753,12 +2228,12 @@ describe("importExport", () => {
// Wait for initialization to complete
await realProviderSettingsManager.initialize()
- // Save a claude-code provider config with token fields
+ // Save a deepseek provider config with token fields
await realProviderSettingsManager.saveConfig(providerName, {
- apiProvider: "claude-code" as ProviderName,
+ apiProvider: "deepseek" as ProviderName,
apiModelId: modelId,
id: providerId,
- apiKey: "test-key",
+ deepSeekApiKey: "test-key",
modelMaxTokens: 4096, // This should be removed during export
modelMaxThinkingTokens: 2048, // This should be removed during export
})
diff --git a/src/core/config/importExport.ts b/src/core/config/importExport.ts
index c3d6f9c215..542f5b0743 100644
--- a/src/core/config/importExport.ts
+++ b/src/core/config/importExport.ts
@@ -6,12 +6,18 @@ import fs from "fs/promises"
import * as vscode from "vscode"
import { z, ZodError } from "zod"
-import { globalSettingsSchema } from "@roo-code/types"
+import {
+ globalSettingsSchema,
+ providerSettingsWithIdSchema,
+ isProviderName,
+ type ProviderSettingsWithId,
+} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { ProviderSettingsManager, providerProfilesSchema } from "./ProviderSettingsManager"
import { ContextProxy } from "./ContextProxy"
import { CustomModesManager } from "./CustomModesManager"
+import { resolveDefaultSaveUri, saveLastExportPath } from "../../utils/export"
import { t } from "../../i18n"
export type ImportOptions = {
@@ -31,36 +37,119 @@ type ImportWithProviderOptions = ImportOptions & {
}
}
+/**
+ * Sanitizes a provider config by resetting invalid/removed apiProvider values.
+ * Returns the sanitized config and a warning message if the provider was invalid.
+ */
+function sanitizeProviderConfig(configName: string, apiConfig: unknown): { config: unknown; warning?: string } {
+ if (typeof apiConfig !== "object" || apiConfig === null) {
+ return { config: apiConfig }
+ }
+
+ const config = apiConfig as Record
+
+ // Check if apiProvider is set and if it's still valid
+ if (config.apiProvider !== undefined && !isProviderName(config.apiProvider)) {
+ const invalidProvider = config.apiProvider
+ // Return a new config object without the invalid apiProvider
+ const { apiProvider, ...restConfig } = config
+ return {
+ config: restConfig,
+ warning: `Profile "${configName}": Invalid provider "${invalidProvider}" was removed. Please reconfigure this profile.`,
+ }
+ }
+
+ return { config: apiConfig }
+}
+
/**
* Imports configuration from a specific file path
* Shares base functionality for import settings for both the manual
- * and automatic settings importing
+ * and automatic settings importing.
+ *
+ * Uses lenient parsing to handle invalid/removed providers gracefully:
+ * - Invalid apiProvider values are removed (profile is kept but needs reconfiguration)
+ * - Completely invalid profiles are skipped
+ * - Warnings are returned for any issues encountered
*/
export async function importSettingsFromPath(
filePath: string,
{ providerSettingsManager, contextProxy, customModesManager }: ImportOptions,
) {
- const schema = z.object({
- providerProfiles: providerProfilesSchema,
+ // Use a lenient schema that accepts any apiConfigs, then validate each individually
+ const lenientProviderProfilesSchema = providerProfilesSchema.extend({
+ apiConfigs: z.record(z.string(), z.any()),
+ })
+
+ const lenientSchema = z.object({
+ providerProfiles: lenientProviderProfilesSchema,
globalSettings: globalSettingsSchema.optional(),
})
try {
const previousProviderProfiles = await providerSettingsManager.export()
- const { providerProfiles: newProviderProfiles, globalSettings = {} } = schema.parse(
- JSON.parse(await fs.readFile(filePath, "utf-8")),
- )
+ const rawData = JSON.parse(await fs.readFile(filePath, "utf-8"))
+ const { providerProfiles: rawProviderProfiles, globalSettings = {} } = lenientSchema.parse(rawData)
+
+ // Track warnings for profiles that had issues
+ const warnings: string[] = []
+ const validApiConfigs: Record = {}
+
+ // Process each apiConfig individually with sanitization
+ for (const [configName, rawConfig] of Object.entries(rawProviderProfiles.apiConfigs)) {
+ // First sanitize to handle invalid apiProvider values
+ const { config: sanitizedConfig, warning } = sanitizeProviderConfig(configName, rawConfig)
+ if (warning) {
+ warnings.push(warning)
+ }
+
+ // Then validate the sanitized config
+ const result = providerSettingsWithIdSchema.safeParse(sanitizedConfig)
+ if (result.success) {
+ validApiConfigs[configName] = result.data
+ } else {
+ // Profile is completely invalid - skip it
+ const issues = result.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ")
+ warnings.push(`Profile "${configName}" was skipped: ${issues}`)
+ }
+ }
+
+ // If no valid configs were imported and there were issues, report them
+ if (Object.keys(validApiConfigs).length === 0 && warnings.length > 0) {
+ return {
+ success: false,
+ error: `No valid profiles could be imported:\n${warnings.join("\n")}`,
+ }
+ }
+
+ // Determine the currentApiConfigName:
+ // 1. If the imported currentApiConfigName exists in validApiConfigs, use it
+ // 2. Otherwise, fall back to the first valid imported profile
+ // 3. If no valid profiles were imported, keep the previous currentApiConfigName
+ let currentApiConfigName = rawProviderProfiles.currentApiConfigName
+ const validProfileNames = Object.keys(validApiConfigs)
+ if (!validApiConfigs[currentApiConfigName]) {
+ if (validProfileNames.length > 0) {
+ currentApiConfigName = validProfileNames[0]
+ warnings.push(
+ `Profile "${rawProviderProfiles.currentApiConfigName}" was not available; defaulting to "${currentApiConfigName}".`,
+ )
+ } else {
+ // No valid imported profiles; keep the existing currentApiConfigName
+ currentApiConfigName = previousProviderProfiles.currentApiConfigName
+ }
+ }
const providerProfiles = {
- currentApiConfigName: newProviderProfiles.currentApiConfigName,
+ currentApiConfigName,
apiConfigs: {
...previousProviderProfiles.apiConfigs,
- ...newProviderProfiles.apiConfigs,
+ ...validApiConfigs,
},
modeApiConfigs: {
...previousProviderProfiles.modeApiConfigs,
- ...newProviderProfiles.modeApiConfigs,
+ ...rawProviderProfiles.modeApiConfigs,
},
}
@@ -88,7 +177,12 @@ export async function importSettingsFromPath(
contextProxy.setValue("listApiConfigMeta", await providerSettingsManager.listConfig())
- return { providerProfiles, globalSettings, success: true }
+ return {
+ providerProfiles,
+ globalSettings,
+ success: true,
+ warnings: warnings.length > 0 ? warnings : undefined,
+ }
} catch (e) {
let error = "Unknown error"
@@ -109,9 +203,16 @@ export async function importSettingsFromPath(
* @returns Promise resolving to import result
*/
export const importSettings = async ({ providerSettingsManager, contextProxy, customModesManager }: ImportOptions) => {
+ // Use the last export path as a sensible default, falling back to Downloads
+ const defaultUri = resolveDefaultSaveUri(contextProxy, "lastSettingsExportPath", "roo-code-settings.json", {
+ useWorkspace: false,
+ fallbackDir: path.join(os.homedir(), "Downloads"),
+ })
+
const uris = await vscode.window.showOpenDialog({
filters: { JSON: ["json"] },
canSelectMany: false,
+ defaultUri,
})
if (!uris) {
@@ -143,15 +244,22 @@ export const importSettingsFromFile = async (
}
export const exportSettings = async ({ providerSettingsManager, contextProxy }: ExportOptions) => {
+ const defaultUri = await resolveDefaultSaveUri(contextProxy, "lastSettingsExportPath", "roo-code-settings.json", {
+ useWorkspace: false,
+ fallbackDir: path.join(os.homedir(), "Downloads"),
+ })
+
const uri = await vscode.window.showSaveDialog({
filters: { JSON: ["json"] },
- defaultUri: vscode.Uri.file(path.join(os.homedir(), "Documents", "roo-code-settings.json")),
+ defaultUri,
})
if (!uri) {
return
}
+ await saveLastExportPath(contextProxy, "lastSettingsExportPath", uri)
+
try {
const providerProfiles = await providerSettingsManager.export()
const globalSettings = await contextProxy.export()
@@ -211,7 +319,22 @@ export const importSettingsWithFeedback = async (
if (result.success) {
provider.settingsImportedAt = Date.now()
await provider.postStateToWebview()
- await vscode.window.showInformationMessage(t("common:info.settings_imported"))
+
+ // Show warnings if any profiles had issues but were still imported (with modifications)
+ if (result.warnings && result.warnings.length > 0) {
+ // Log full details to the console for debugging
+ console.warn("Settings import completed with warnings:", result.warnings)
+
+ // Show a short summary in the toast notification
+ const count = result.warnings.length
+ const summary =
+ count === 1 ? `1 profile had issues during import.` : `${count} profiles had issues during import.`
+ await vscode.window.showWarningMessage(
+ `${t("common:info.settings_imported")} ${summary} See Developer Tools console for details.`,
+ )
+ } else {
+ await vscode.window.showInformationMessage(t("common:info.settings_imported"))
+ }
} else if (result.error) {
await vscode.window.showErrorMessage(t("common:errors.settings_import_failed", { error: result.error }))
}
diff --git a/src/core/context-management/__tests__/context-management.spec.ts b/src/core/context-management/__tests__/context-management.spec.ts
index 3ee36fc595..9950ec536b 100644
--- a/src/core/context-management/__tests__/context-management.spec.ts
+++ b/src/core/context-management/__tests__/context-management.spec.ts
@@ -578,8 +578,8 @@ describe("Context Management", () => {
const mockSummarizeResponse: condenseModule.SummarizeResponse = {
messages: [
{ role: "user", content: "First message" },
- { role: "assistant", content: mockSummary, isSummary: true },
- { role: "user", content: "Last message" },
+ { role: "user", content: mockSummary, isSummary: true },
+ { role: "assistant", content: "Last message" },
],
summary: mockSummary,
cost: mockCost,
@@ -612,17 +612,13 @@ describe("Context Management", () => {
})
// Verify summarizeConversation was called with the right parameters
- expect(summarizeSpy).toHaveBeenCalledWith(
- messagesWithSmallContent,
- mockApiHandler,
- "System prompt",
+ expect(summarizeSpy).toHaveBeenCalledWith({
+ messages: messagesWithSmallContent,
+ apiHandler: mockApiHandler,
+ systemPrompt: "System prompt",
taskId,
- 70001,
- true,
- undefined, // customCondensingPrompt
- undefined, // condensingApiHandler
- undefined, // useNativeTools
- )
+ isAutomaticTrigger: true,
+ })
// Verify the result contains the summary information
expect(result).toMatchObject({
@@ -752,8 +748,8 @@ describe("Context Management", () => {
const mockSummarizeResponse: condenseModule.SummarizeResponse = {
messages: [
{ role: "user", content: "First message" },
- { role: "assistant", content: mockSummary, isSummary: true },
- { role: "user", content: "Last message" },
+ { role: "user", content: mockSummary, isSummary: true },
+ { role: "assistant", content: "Last message" },
],
summary: mockSummary,
cost: mockCost,
@@ -788,17 +784,13 @@ describe("Context Management", () => {
})
// Verify summarizeConversation was called with the right parameters
- expect(summarizeSpy).toHaveBeenCalledWith(
- messagesWithSmallContent,
- mockApiHandler,
- "System prompt",
+ expect(summarizeSpy).toHaveBeenCalledWith({
+ messages: messagesWithSmallContent,
+ apiHandler: mockApiHandler,
+ systemPrompt: "System prompt",
taskId,
- 60000,
- true,
- undefined, // customCondensingPrompt
- undefined, // condensingApiHandler
- undefined, // useNativeTools
- )
+ isAutomaticTrigger: true,
+ })
// Verify the result contains the summary information
expect(result).toMatchObject({
@@ -856,6 +848,215 @@ describe("Context Management", () => {
})
})
+ /**
+ * Tests for filesReadByRoo being passed to summarizeConversation
+ */
+ describe("filesReadByRoo parameters", () => {
+ const createModelInfo = (contextWindow: number, maxTokens?: number): ModelInfo => ({
+ contextWindow,
+ supportsPromptCache: true,
+ maxTokens,
+ })
+
+ const messages: ApiMessage[] = [
+ { role: "user", content: "First message" },
+ { role: "assistant", content: "Second message" },
+ { role: "user", content: "Third message" },
+ { role: "assistant", content: "Fourth message" },
+ { role: "user", content: "Fifth message" },
+ ]
+
+ it("should pass filesReadByRoo, cwd, and rooIgnoreController to summarizeConversation when provided", async () => {
+ // Mock the summarizeConversation function
+ const mockSummary = "Summary with folded context"
+ const mockCost = 0.05
+ const mockSummarizeResponse: condenseModule.SummarizeResponse = {
+ messages: [
+ { role: "user", content: "First message" },
+ { role: "assistant", content: mockSummary, isSummary: true },
+ { role: "user", content: "Last message" },
+ ],
+ summary: mockSummary,
+ cost: mockCost,
+ newContextTokens: 100,
+ }
+
+ const summarizeSpy = vi
+ .spyOn(condenseModule, "summarizeConversation")
+ .mockResolvedValue(mockSummarizeResponse)
+
+ const modelInfo = createModelInfo(100000, 30000)
+ const totalTokens = 70001 // Above threshold
+ const messagesWithSmallContent = [
+ ...messages.slice(0, -1),
+ { ...messages[messages.length - 1], content: "" },
+ ]
+
+ const filesReadByRoo = ["src/test.ts", "src/utils.ts"]
+ const cwd = "/test/project"
+ const mockRooIgnoreController = {
+ filterPaths: vi.fn(),
+ } as unknown as import("../../ignore/RooIgnoreController").RooIgnoreController
+
+ const result = await manageContext({
+ messages: messagesWithSmallContent,
+ totalTokens,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ apiHandler: mockApiHandler,
+ autoCondenseContext: true,
+ autoCondenseContextPercent: 100,
+ systemPrompt: "System prompt",
+ taskId,
+ profileThresholds: {},
+ currentProfileId: "default",
+ filesReadByRoo,
+ cwd,
+ rooIgnoreController: mockRooIgnoreController,
+ })
+
+ // Verify summarizeConversation was called with filesReadByRoo, cwd, and rooIgnoreController
+ expect(summarizeSpy).toHaveBeenCalledWith({
+ messages: messagesWithSmallContent,
+ apiHandler: mockApiHandler,
+ systemPrompt: "System prompt",
+ taskId,
+ isAutomaticTrigger: true,
+ filesReadByRoo,
+ cwd,
+ rooIgnoreController: mockRooIgnoreController,
+ })
+
+ // Verify the result contains the summary information
+ expect(result).toMatchObject({
+ messages: mockSummarizeResponse.messages,
+ summary: mockSummary,
+ cost: mockCost,
+ prevContextTokens: totalTokens,
+ })
+
+ // Clean up
+ summarizeSpy.mockRestore()
+ })
+
+ it("should pass undefined filesReadByRoo parameters when not provided", async () => {
+ // Mock the summarizeConversation function
+ const mockSummary = "Summary without folded context"
+ const mockCost = 0.03
+ const mockSummarizeResponse: condenseModule.SummarizeResponse = {
+ messages: [
+ { role: "user", content: "First message" },
+ { role: "assistant", content: mockSummary, isSummary: true },
+ { role: "user", content: "Last message" },
+ ],
+ summary: mockSummary,
+ cost: mockCost,
+ newContextTokens: 80,
+ }
+
+ const summarizeSpy = vi
+ .spyOn(condenseModule, "summarizeConversation")
+ .mockResolvedValue(mockSummarizeResponse)
+
+ const modelInfo = createModelInfo(100000, 30000)
+ const totalTokens = 70001 // Above threshold
+ const messagesWithSmallContent = [
+ ...messages.slice(0, -1),
+ { ...messages[messages.length - 1], content: "" },
+ ]
+
+ const result = await manageContext({
+ messages: messagesWithSmallContent,
+ totalTokens,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ apiHandler: mockApiHandler,
+ autoCondenseContext: true,
+ autoCondenseContextPercent: 100,
+ systemPrompt: "System prompt",
+ taskId,
+ profileThresholds: {},
+ currentProfileId: "default",
+ // filesReadByRoo, cwd, rooIgnoreController are NOT provided
+ })
+
+ // Verify summarizeConversation was called with undefined parameters
+ expect(summarizeSpy).toHaveBeenCalledWith({
+ messages: messagesWithSmallContent,
+ apiHandler: mockApiHandler,
+ systemPrompt: "System prompt",
+ taskId,
+ isAutomaticTrigger: true,
+ })
+
+ // Verify the result
+ expect(result).toMatchObject({
+ summary: mockSummary,
+ cost: mockCost,
+ })
+
+ // Clean up
+ summarizeSpy.mockRestore()
+ })
+
+ it("should pass empty array filesReadByRoo when provided as empty", async () => {
+ // Mock the summarizeConversation function
+ const mockSummary = "Summary with empty file list"
+ const mockCost = 0.04
+ const mockSummarizeResponse: condenseModule.SummarizeResponse = {
+ messages: [
+ { role: "user", content: "First message" },
+ { role: "assistant", content: mockSummary, isSummary: true },
+ { role: "user", content: "Last message" },
+ ],
+ summary: mockSummary,
+ cost: mockCost,
+ newContextTokens: 90,
+ }
+
+ const summarizeSpy = vi
+ .spyOn(condenseModule, "summarizeConversation")
+ .mockResolvedValue(mockSummarizeResponse)
+
+ const modelInfo = createModelInfo(100000, 30000)
+ const totalTokens = 70001 // Above threshold
+ const messagesWithSmallContent = [
+ ...messages.slice(0, -1),
+ { ...messages[messages.length - 1], content: "" },
+ ]
+
+ const result = await manageContext({
+ messages: messagesWithSmallContent,
+ totalTokens,
+ contextWindow: modelInfo.contextWindow,
+ maxTokens: modelInfo.maxTokens,
+ apiHandler: mockApiHandler,
+ autoCondenseContext: true,
+ autoCondenseContextPercent: 100,
+ systemPrompt: "System prompt",
+ taskId,
+ profileThresholds: {},
+ currentProfileId: "default",
+ filesReadByRoo: [], // Empty array
+ cwd: "/test/project",
+ })
+
+ // Verify summarizeConversation was called with empty array
+ expect(summarizeSpy).toHaveBeenCalledWith({
+ messages: messagesWithSmallContent,
+ apiHandler: mockApiHandler,
+ systemPrompt: "System prompt",
+ taskId,
+ isAutomaticTrigger: true,
+ filesReadByRoo: [],
+ cwd: "/test/project",
+ })
+
+ // Clean up
+ summarizeSpy.mockRestore()
+ })
+ })
+
/**
* Tests for profile-specific thresholds functionality
*/
@@ -901,8 +1102,8 @@ describe("Context Management", () => {
const mockSummarizeResponse: condenseModule.SummarizeResponse = {
messages: [
{ role: "user", content: "First message" },
- { role: "assistant", content: mockSummary, isSummary: true },
- { role: "user", content: "Last message" },
+ { role: "user", content: mockSummary, isSummary: true },
+ { role: "assistant", content: "Last message" },
],
summary: mockSummary,
cost: mockCost,
@@ -967,8 +1168,8 @@ describe("Context Management", () => {
const mockSummarizeResponse: condenseModule.SummarizeResponse = {
messages: [
{ role: "user", content: "First message" },
- { role: "assistant", content: mockSummary, isSummary: true },
- { role: "user", content: "Last message" },
+ { role: "user", content: mockSummary, isSummary: true },
+ { role: "assistant", content: "Last message" },
],
summary: mockSummary,
cost: mockCost,
diff --git a/src/core/context-management/index.ts b/src/core/context-management/index.ts
index a94a53c9d5..243d7bd797 100644
--- a/src/core/context-management/index.ts
+++ b/src/core/context-management/index.ts
@@ -3,10 +3,11 @@ 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"
+import { RooIgnoreController } from "../ignore/RooIgnoreController"
/**
* Context Management
@@ -216,10 +217,18 @@ export type ContextManagementOptions = {
systemPrompt: string
taskId: string
customCondensingPrompt?: string
- condensingApiHandler?: ApiHandler
profileThresholds: Record
currentProfileId: string
- useNativeTools?: boolean
+ /** Optional metadata to pass through to the condensing API call (tools, taskId, etc.) */
+ metadata?: ApiHandlerCreateMessageMetadata
+ /** Optional environment details string to include in the condensed summary */
+ environmentDetails?: string
+ /** Optional array of file paths read by Roo during the task (will be folded via tree-sitter) */
+ filesReadByRoo?: string[]
+ /** Optional current working directory for resolving file paths (required if filesReadByRoo is provided) */
+ cwd?: string
+ /** Optional controller for file access validation */
+ rooIgnoreController?: RooIgnoreController
}
export type ContextManagementResult = SummarizeResponse & {
@@ -246,12 +255,16 @@ export async function manageContext({
systemPrompt,
taskId,
customCondensingPrompt,
- condensingApiHandler,
profileThresholds,
currentProfileId,
- useNativeTools,
+ metadata,
+ environmentDetails,
+ filesReadByRoo,
+ cwd,
+ rooIgnoreController,
}: ContextManagementOptions): Promise {
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
@@ -294,19 +307,22 @@ export async function manageContext({
const contextPercent = (100 * prevContextTokens) / contextWindow
if (contextPercent >= effectiveThreshold || prevContextTokens > allowedTokens) {
// Attempt to intelligently condense the context
- const result = await summarizeConversation(
+ const result = await summarizeConversation({
messages,
apiHandler,
systemPrompt,
taskId,
- prevContextTokens,
- true, // automatic trigger
+ isAutomaticTrigger: true,
customCondensingPrompt,
- condensingApiHandler,
- useNativeTools,
- )
+ metadata,
+ environmentDetails,
+ filesReadByRoo,
+ cwd,
+ rooIgnoreController,
+ })
if (result.error) {
error = result.error
+ errorDetails = result.errorDetails
cost = result.cost
} else {
return { ...result, prevContextTokens }
@@ -349,11 +365,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 }
}
diff --git a/src/core/context-tracking/FileContextTracker.ts b/src/core/context-tracking/FileContextTracker.ts
index 5741b62cfc..4c5640afdf 100644
--- a/src/core/context-tracking/FileContextTracker.ts
+++ b/src/core/context-tracking/FileContextTracker.ts
@@ -206,6 +206,59 @@ export class FileContextTracker {
return files
}
+ /**
+ * Gets a list of unique file paths that Roo has read during this task.
+ * Files are sorted by most recently read first, so if there's a character
+ * budget during folded context generation, the most relevant (recent) files
+ * are prioritized.
+ *
+ * @param sinceTimestamp - Optional timestamp to filter files read after this time
+ * @returns Array of unique file paths that have been read, most recent first
+ */
+ async getFilesReadByRoo(sinceTimestamp?: number): Promise {
+ try {
+ const metadata = await this.getTaskMetadata(this.taskId)
+
+ const readEntries = metadata.files_in_context.filter((entry) => {
+ // Only include files that were read by Roo (not user edits)
+ const isReadByRoo = entry.record_source === "read_tool" || entry.record_source === "file_mentioned"
+ if (!isReadByRoo) {
+ return false
+ }
+
+ // If sinceTimestamp is provided, only include files read after that time
+ if (sinceTimestamp && entry.roo_read_date) {
+ return entry.roo_read_date >= sinceTimestamp
+ }
+
+ return true
+ })
+
+ // Sort by roo_read_date descending (most recent first)
+ // Entries without a date go to the end
+ readEntries.sort((a, b) => {
+ const dateA = a.roo_read_date ?? 0
+ const dateB = b.roo_read_date ?? 0
+ return dateB - dateA
+ })
+
+ // Deduplicate while preserving order (first occurrence = most recent read)
+ const seen = new Set()
+ const uniquePaths: string[] = []
+ for (const entry of readEntries) {
+ if (!seen.has(entry.path)) {
+ seen.add(entry.path)
+ uniquePaths.push(entry.path)
+ }
+ }
+
+ return uniquePaths
+ } catch (error) {
+ console.error("Failed to get files read by Roo:", error)
+ return []
+ }
+ }
+
getAndClearCheckpointPossibleFile(): string[] {
const files = Array.from(this.checkpointPossibleFiles)
this.checkpointPossibleFiles.clear()
diff --git a/src/core/diff/strategies/__tests__/multi-file-search-replace-8char.spec.ts b/src/core/diff/strategies/__tests__/multi-file-search-replace-8char.spec.ts
deleted file mode 100644
index 4d5d29ca39..0000000000
--- a/src/core/diff/strategies/__tests__/multi-file-search-replace-8char.spec.ts
+++ /dev/null
@@ -1,189 +0,0 @@
-import { describe, it, expect } from "vitest"
-import { MultiFileSearchReplaceDiffStrategy } from "../multi-file-search-replace"
-
-describe("MultiFileSearchReplaceDiffStrategy - 8-character marker support", () => {
- it("should handle 8 '<' characters in SEARCH marker (PR #9456 use case)", async () => {
- const strategy = new MultiFileSearchReplaceDiffStrategy()
- const originalContent = "line 1\nline 2\nline 3"
-
- const diff = `<<<<<<<< SEARCH
-:start_line:1
--------
-line 1
-=======
-modified line 1
->>>>>>>> REPLACE`
-
- const result = await strategy.applyDiff(originalContent, diff)
-
- expect(result.success).toBe(true)
- if (result.success) {
- expect(result.content).toBe("modified line 1\nline 2\nline 3")
- }
- })
-
- it("should handle 7 '<' characters in SEARCH marker (standard)", async () => {
- const strategy = new MultiFileSearchReplaceDiffStrategy()
- const originalContent = "line 1\nline 2\nline 3"
-
- const diff = `<<<<<<< SEARCH
-:start_line:1
--------
-line 1
-=======
-modified line 1
->>>>>>> REPLACE`
-
- const result = await strategy.applyDiff(originalContent, diff)
-
- expect(result.success).toBe(true)
- if (result.success) {
- expect(result.content).toBe("modified line 1\nline 2\nline 3")
- }
- })
-
- it("should handle 8 '>' characters in REPLACE marker", async () => {
- const strategy = new MultiFileSearchReplaceDiffStrategy()
- const originalContent = "line 1\nline 2\nline 3"
-
- const diff = `<<<<<<< SEARCH
-:start_line:2
--------
-line 2
-=======
-modified line 2
->>>>>>>> REPLACE`
-
- const result = await strategy.applyDiff(originalContent, diff)
-
- expect(result.success).toBe(true)
- if (result.success) {
- expect(result.content).toBe("line 1\nmodified line 2\nline 3")
- }
- })
-
- it("should handle optional '<' at end of REPLACE marker", async () => {
- const strategy = new MultiFileSearchReplaceDiffStrategy()
- const originalContent = "line 1\nline 2\nline 3"
-
- const diff = `<<<<<<< SEARCH
-:start_line:3
--------
-line 3
-=======
-modified line 3
->>>>>>> REPLACE<`
-
- const result = await strategy.applyDiff(originalContent, diff)
-
- expect(result.success).toBe(true)
- if (result.success) {
- expect(result.content).toBe("line 1\nline 2\nmodified line 3")
- }
- })
-
- it("should handle mixed 7 and 8 character markers in same diff", async () => {
- const strategy = new MultiFileSearchReplaceDiffStrategy()
- const originalContent = "line 1\nline 2\nline 3"
-
- const diff = `<<<<<<<< SEARCH
-:start_line:1
--------
-line 1
-=======
-modified line 1
->>>>>>> REPLACE
-
-<<<<<<< SEARCH
-:start_line:3
--------
-line 3
-=======
-modified line 3
->>>>>>>> REPLACE`
-
- const result = await strategy.applyDiff(originalContent, diff)
-
- expect(result.success).toBe(true)
- if (result.success) {
- expect(result.content).toBe("modified line 1\nline 2\nmodified line 3")
- }
- })
-
- it("should reject markers with too many characters (9+)", async () => {
- const strategy = new MultiFileSearchReplaceDiffStrategy()
- const originalContent = "line 1\nline 2\nline 3"
-
- const diff = `<<<<<<<<< SEARCH
-:start_line:1
--------
-line 1
-=======
-modified line 1
->>>>>>> REPLACE`
-
- const result = await strategy.applyDiff(originalContent, diff)
-
- expect(result.success).toBe(false)
- if (!result.success) {
- expect(result.error).toContain("Diff block is malformed")
- }
- })
-
- it("should reject markers with too few characters (6-)", async () => {
- const strategy = new MultiFileSearchReplaceDiffStrategy()
- const originalContent = "line 1\nline 2\nline 3"
-
- const diff = `<<<<<< SEARCH
-:start_line:1
--------
-line 1
-=======
-modified line 1
->>>>>>> REPLACE`
-
- const result = await strategy.applyDiff(originalContent, diff)
-
- expect(result.success).toBe(false)
- if (!result.success) {
- expect(result.error).toContain("Diff block is malformed")
- }
- })
-
- it("should handle validation with 8 character markers", () => {
- const strategy = new MultiFileSearchReplaceDiffStrategy()
-
- const diff = `<<<<<<<< SEARCH
-:start_line:1
--------
-content
-=======
-new content
->>>>>>>> REPLACE`
-
- const result = strategy["validateMarkerSequencing"](diff)
-
- expect(result.success).toBe(true)
- })
-
- it("should detect merge conflict with 8 character prefix", () => {
- const strategy = new MultiFileSearchReplaceDiffStrategy()
-
- const diff = `<<<<<<<< SEARCH
-:start_line:1
--------
-content
-<<<<<<<< HEAD
-conflict content
-=======
-new content
->>>>>>>> REPLACE`
-
- const result = strategy["validateMarkerSequencing"](diff)
-
- expect(result.success).toBe(false)
- if (!result.success) {
- expect(result.error).toContain("merge conflict")
- }
- })
-})
diff --git a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts
index b25286f5fa..f06f3f406f 100644
--- a/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts
+++ b/src/core/diff/strategies/__tests__/multi-search-replace.spec.ts
@@ -1041,29 +1041,6 @@ function sum(a, b) {
})
})
- describe("getToolDescription", () => {
- let strategy: MultiSearchReplaceDiffStrategy
-
- beforeEach(() => {
- strategy = new MultiSearchReplaceDiffStrategy()
- })
-
- it("should include the current workspace directory", async () => {
- const cwd = "/test/dir"
- const description = await strategy.getToolDescription({ cwd })
- expect(description).toContain(`relative to the current workspace directory ${cwd}`)
- })
-
- it("should include required format elements", async () => {
- const description = await strategy.getToolDescription({ cwd: "/test" })
- expect(description).toContain("<<<<<<< SEARCH")
- expect(description).toContain("=======")
- expect(description).toContain(">>>>>>> REPLACE")
- expect(description).toContain("")
- expect(description).toContain("")
- })
- })
-
describe("line marker validation in REPLACE sections", () => {
let strategy: MultiSearchReplaceDiffStrategy
diff --git a/src/core/diff/strategies/multi-file-search-replace.ts b/src/core/diff/strategies/multi-file-search-replace.ts
deleted file mode 100644
index 1236a98fbb..0000000000
--- a/src/core/diff/strategies/multi-file-search-replace.ts
+++ /dev/null
@@ -1,741 +0,0 @@
-import { distance } from "fastest-levenshtein"
-import { ToolProgressStatus } from "@roo-code/types"
-
-import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text"
-import { ToolUse, DiffStrategy, DiffResult } from "../../../shared/tools"
-import { normalizeString } from "../../../utils/text-normalization"
-
-const BUFFER_LINES = 40 // Number of extra context lines to show before and after matches
-
-function getSimilarity(original: string, search: string): number {
- // Empty searches are no longer supported
- if (search === "") {
- return 0
- }
-
- // Use the normalizeString utility to handle smart quotes and other special characters
- const normalizedOriginal = normalizeString(original)
- const normalizedSearch = normalizeString(search)
-
- if (normalizedOriginal === normalizedSearch) {
- return 1
- }
-
- // Calculate Levenshtein distance using fastest-levenshtein's distance function
- const dist = distance(normalizedOriginal, normalizedSearch)
-
- // Calculate similarity ratio (0 to 1, where 1 is an exact match)
- const maxLength = Math.max(normalizedOriginal.length, normalizedSearch.length)
- return 1 - dist / maxLength
-}
-
-/**
- * Performs a "middle-out" search of `lines` (between [startIndex, endIndex]) to find
- * the slice that is most similar to `searchChunk`. Returns the best score, index, and matched text.
- */
-function fuzzySearch(lines: string[], searchChunk: string, startIndex: number, endIndex: number) {
- let bestScore = 0
- let bestMatchIndex = -1
- let bestMatchContent = ""
-
- const searchLen = searchChunk.split(/\r?\n/).length
-
- // Middle-out from the midpoint
- const midPoint = Math.floor((startIndex + endIndex) / 2)
- let leftIndex = midPoint
- let rightIndex = midPoint + 1
-
- while (leftIndex >= startIndex || rightIndex <= endIndex - searchLen) {
- if (leftIndex >= startIndex) {
- const originalChunk = lines.slice(leftIndex, leftIndex + searchLen).join("\n")
- const similarity = getSimilarity(originalChunk, searchChunk)
-
- if (similarity > bestScore) {
- bestScore = similarity
- bestMatchIndex = leftIndex
- bestMatchContent = originalChunk
- }
- leftIndex--
- }
-
- if (rightIndex <= endIndex - searchLen) {
- const originalChunk = lines.slice(rightIndex, rightIndex + searchLen).join("\n")
- const similarity = getSimilarity(originalChunk, searchChunk)
-
- if (similarity > bestScore) {
- bestScore = similarity
- bestMatchIndex = rightIndex
- bestMatchContent = originalChunk
- }
- rightIndex++
- }
- }
-
- return { bestScore, bestMatchIndex, bestMatchContent }
-}
-
-export class MultiFileSearchReplaceDiffStrategy implements DiffStrategy {
- private fuzzyThreshold: number
- private bufferLines: number
-
- getName(): string {
- return "MultiFileSearchReplace"
- }
-
- constructor(fuzzyThreshold?: number, bufferLines?: number) {
- // Use provided threshold or default to exact matching (1.0)
- // Note: fuzzyThreshold is inverted in UI (0% = 1.0, 10% = 0.9)
- // so we use it directly here
- this.fuzzyThreshold = fuzzyThreshold ?? 1.0
- this.bufferLines = bufferLines ?? BUFFER_LINES
- }
-
- getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string {
- return `## apply_diff
-
-Description: Request to apply PRECISE, TARGETED modifications to one or more files by searching for specific sections of content and replacing them. This tool is for SURGICAL EDITS ONLY - specific changes to existing code. This tool supports both single-file and multi-file operations, allowing you to make changes across multiple files in a single request.
-
-**IMPORTANT: You MUST use multiple files in a single operation whenever possible to maximize efficiency and minimize back-and-forth.**
-
-You can perform multiple distinct search and replace operations within a single \`apply_diff\` call by providing multiple SEARCH/REPLACE blocks in the \`diff\` parameter. This is the preferred way to make several targeted changes efficiently.
-
-The SEARCH section must exactly match existing content including whitespace and indentation.
-If you're not confident in the exact content to search for, use the read_file tool first to get the exact content.
-When applying the diffs, be extra careful to remember to change any closing brackets or other syntax that may be affected by the diff farther down in the file.
-ALWAYS make as many changes in a single 'apply_diff' request as possible using multiple SEARCH/REPLACE blocks
-
-Parameters:
-- args: Contains one or more file elements, where each file contains:
- - path: (required) The path of the file to modify (relative to the current workspace directory ${args.cwd})
- - diff: (required) One or more diff elements containing:
- - content: (required) The search/replace block defining the changes.
- - start_line: (required) The line number of original content where the search block starts.
-
-Diff format:
-\`\`\`
-<<<<<<< SEARCH
-:start_line: (required) The line number of original content where the search block starts.
--------
-[exact content to find including whitespace]
-=======
-[new content to replace with]
->>>>>>> REPLACE
-\`\`\`
-
-Example:
-
-Original file:
-\`\`\`
-1 | def calculate_total(items):
-2 | total = 0
-3 | for item in items:
-4 | total += item
-5 | return total
-\`\`\`
-
-Search/Replace content:
-
-
-
- eg.file.py
-
- >>>>>> REPLACE
-]]>
-
-
-
-
-
-Search/Replace content with multi edits across multiple files:
-
-
-
- eg.file.py
-
- >>>>>> REPLACE
-]]>
-
-
- >>>>>> REPLACE
-]]>
-
-
-
- eg.file2.py
-
- >>>>>> REPLACE
-]]>
-
-
-
-
-
-
-Usage:
-
-
-
- File path here
-
-
-Your search/replace content here
-You can use multi search/replace block in one diff block, but make sure to include the line numbers for each block.
-Only use a single line of '=======' between search and replacement content, because multiple '=======' will corrupt the file.
-
- 1
-
-
-
- Another file path
-
-
-Another search/replace content here
-You can apply changes to multiple files in a single request.
-Each file requires its own path, start_line, and diff elements.
-
- 5
-
-
-
-`
- }
-
- private unescapeMarkers(content: string): string {
- return content
- .replace(/^\\<<<<<<>>>>>>/gm, ">>>>>>>")
- .replace(/^\\-------/gm, "-------")
- .replace(/^\\:end_line:/gm, ":end_line:")
- .replace(/^\\:start_line:/gm, ":start_line:")
- }
-
- private validateMarkerSequencing(diffContent: string): { success: boolean; error?: string } {
- enum State {
- START,
- AFTER_SEARCH,
- AFTER_SEPARATOR,
- }
-
- const state = { current: State.START, line: 0 }
-
- // Pattern allows optional extra '<' or '>' for SEARCH to handle AI-generated diffs
- // (e.g., Sonnet 4 sometimes adds extra markers)
- // Using explicit alternation instead of quantifiers to avoid regex backtracking
- const SEARCH_PATTERN = /^(?:<<<<<<< |<<<<<<<< )SEARCH>?$/
- const SEARCH = "<<<<<<< SEARCH" // Simplified for display
- const SEP = "======="
- // Pattern allows optional extra '>' or '<' for REPLACE
- const REPLACE_PATTERN = /^(?:>>>>>>> |>>>>>>>> )REPLACE$/
- const REPLACE = ">>>>>>> REPLACE" // Simplified for display
- const SEARCH_PREFIX_PATTERN = /^(?:<<<<<<< |<<<<<<<< )/
- const REPLACE_PREFIX_PATTERN = /^(?:>>>>>>> |>>>>>>>> )/
-
- const reportMergeConflictError = (found: string, _expected: string) => ({
- success: false,
- error:
- `ERROR: Special marker '${found}' found in your diff content at line ${state.line}:\n` +
- "\n" +
- `When removing merge conflict markers like '${found}' from files, you MUST escape them\n` +
- "in your SEARCH section by prepending a backslash (\\) at the beginning of the line:\n" +
- "\n" +
- "CORRECT FORMAT:\n\n" +
- "<<<<<<< SEARCH\n" +
- "content before\n" +
- `\\${found} <-- Note the backslash here in this example\n` +
- "content after\n" +
- "=======\n" +
- "replacement content\n" +
- ">>>>>>> REPLACE\n" +
- "\n" +
- "Without escaping, the system confuses your content with diff syntax markers.\n" +
- "You may use multiple diff blocks in a single diff request, but ANY of ONLY the following separators that occur within SEARCH or REPLACE content must be escaped, as follows:\n" +
- `\\${SEARCH}\n` +
- `\\${SEP}\n` +
- `\\${REPLACE}\n`,
- })
-
- const reportInvalidDiffError = (found: string, expected: string) => ({
- success: false,
- error:
- `ERROR: Diff block is malformed: marker '${found}' found in your diff content at line ${state.line}. Expected: ${expected}\n` +
- "\n" +
- "CORRECT FORMAT:\n\n" +
- "<<<<<<< SEARCH\n" +
- ":start_line: (required) The line number of original content where the search block starts.\n" +
- "-------\n" +
- "[exact content to find including whitespace]\n" +
- "=======\n" +
- "[new content to replace with]\n" +
- ">>>>>>> REPLACE\n",
- })
-
- const reportLineMarkerInReplaceError = (marker: string) => ({
- success: false,
- error:
- `ERROR: Invalid line marker '${marker}' found in REPLACE section at line ${state.line}\n` +
- "\n" +
- "Line markers (:start_line: and :end_line:) are only allowed in SEARCH sections.\n" +
- "\n" +
- "CORRECT FORMAT:\n" +
- "<<<<<<< SEARCH\n" +
- ":start_line:5\n" +
- "content to find\n" +
- "=======\n" +
- "replacement content\n" +
- ">>>>>>> REPLACE\n" +
- "\n" +
- "INCORRECT FORMAT:\n" +
- "<<<<<<< SEARCH\n" +
- "content to find\n" +
- "=======\n" +
- ":start_line:5 <-- Invalid location\n" +
- "replacement content\n" +
- ">>>>>>> REPLACE\n",
- })
-
- const lines = diffContent.split("\n")
- const searchCount = lines.filter((l) => SEARCH_PATTERN.test(l.trim())).length
- const sepCount = lines.filter((l) => l.trim() === SEP).length
- const replaceCount = lines.filter((l) => REPLACE_PATTERN.test(l.trim())).length
-
- const likelyBadStructure = searchCount !== replaceCount || sepCount < searchCount
-
- for (const line of diffContent.split("\n")) {
- state.line++
- const marker = line.trim()
-
- // Check for line markers in REPLACE sections (but allow escaped ones)
- if (state.current === State.AFTER_SEPARATOR) {
- if (marker.startsWith(":start_line:") && !line.trim().startsWith("\\:start_line:")) {
- return reportLineMarkerInReplaceError(":start_line:")
- }
- if (marker.startsWith(":end_line:") && !line.trim().startsWith("\\:end_line:")) {
- return reportLineMarkerInReplaceError(":end_line:")
- }
- }
-
- switch (state.current) {
- case State.START:
- if (marker === SEP)
- return likelyBadStructure
- ? reportInvalidDiffError(SEP, SEARCH)
- : reportMergeConflictError(SEP, SEARCH)
- if (REPLACE_PATTERN.test(marker)) return reportInvalidDiffError(REPLACE, SEARCH)
- if (REPLACE_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, SEARCH)
- if (SEARCH_PATTERN.test(marker)) state.current = State.AFTER_SEARCH
- else if (SEARCH_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, SEARCH)
- break
-
- case State.AFTER_SEARCH:
- if (SEARCH_PATTERN.test(marker)) return reportInvalidDiffError(SEARCH, SEP)
- if (SEARCH_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, SEARCH)
- if (REPLACE_PATTERN.test(marker)) return reportInvalidDiffError(REPLACE, SEP)
- if (REPLACE_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, SEARCH)
- if (marker === SEP) state.current = State.AFTER_SEPARATOR
- break
-
- case State.AFTER_SEPARATOR:
- if (SEARCH_PATTERN.test(marker)) return reportInvalidDiffError(SEARCH, REPLACE)
- if (SEARCH_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, REPLACE)
- if (marker === SEP)
- return likelyBadStructure
- ? reportInvalidDiffError(SEP, REPLACE)
- : reportMergeConflictError(SEP, REPLACE)
- if (REPLACE_PATTERN.test(marker)) state.current = State.START
- else if (REPLACE_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, REPLACE)
- break
- }
- }
-
- return state.current === State.START
- ? { success: true }
- : {
- success: false,
- error: `ERROR: Unexpected end of sequence: Expected '${
- state.current === State.AFTER_SEARCH ? "=======" : ">>>>>>> REPLACE"
- }' was not found.`,
- }
- }
-
- async applyDiff(
- originalContent: string,
- diffContent: string | Array<{ content: string; startLine?: number }>,
- _paramStartLine?: number,
- _paramEndLine?: number,
- ): Promise {
- // Handle array-based input for multi-file support
- if (Array.isArray(diffContent)) {
- // Process each diff item separately and combine results
- let resultContent = originalContent
- const allFailParts: DiffResult[] = []
- let successCount = 0
-
- for (const diffItem of diffContent) {
- const singleResult = await this.applySingleDiff(resultContent, diffItem.content, diffItem.startLine)
-
- if (singleResult.success && singleResult.content) {
- resultContent = singleResult.content
- successCount++
- } else {
- // If singleResult has failParts, push those directly to avoid nesting
- if (singleResult.failParts && singleResult.failParts.length > 0) {
- allFailParts.push(...singleResult.failParts)
- } else {
- // Otherwise push the single result itself
- allFailParts.push(singleResult)
- }
- }
- }
-
- if (successCount === 0) {
- return {
- success: false,
- error: "Failed to apply any diffs",
- failParts: allFailParts,
- }
- }
-
- return {
- success: true,
- content: resultContent,
- failParts: allFailParts.length > 0 ? allFailParts : undefined,
- }
- }
-
- // Handle string-based input (legacy)
- return this.applySingleDiff(originalContent, diffContent, _paramStartLine)
- }
-
- private async applySingleDiff(
- originalContent: string,
- diffContent: string,
- _paramStartLine?: number,
- ): Promise {
- const validseq = this.validateMarkerSequencing(diffContent)
- if (!validseq.success) {
- return {
- success: false,
- error: validseq.error!,
- }
- }
-
- /* Regex parts:
- 1. (?:^|\n) Ensures the first marker starts at the beginning of the file or right after a newline.
- 2. (??\s*\n Matches "<<<<<<< SEARCH" or "<<<<<<< SEARCH>" or "<<<<<<<< SEARCH" (7 or 8 '<' chars) (ignoring any trailing spaces) – the negative lookbehind makes sure it isn't escaped. Uses explicit alternation to avoid backtracking.
- 3. ((?:\:start_line:\s*(\d+)\s*\n))? Optionally matches a ":start_line:" line. The outer capturing group is group 1 and the inner (\d+) is group 2.
- 4. ((?:\:end_line:\s*(\d+)\s*\n))? Optionally matches a ":end_line:" line. Group 3 is the whole match and group 4 is the digits.
- 5. ((?>>>>>> |>>>>>>>> )REPLACE)(?=\n|$) Matches ">>>>>>> REPLACE" or ">>>>>>> REPLACE<" or ">>>>>>>> REPLACE" (7 or 8 '>' chars) on its own line (and requires a following newline or the end of file). Uses explicit alternation to avoid backtracking.
- */
- let matches = [
- ...diffContent.matchAll(
- /(?:^|\n)(??\s*\n((?:\:start_line:\s*(\d+)\s*\n))?((?:\:end_line:\s*(\d+)\s*\n))?((?>>>>>> |>>>>>>>> )REPLACE)(?=\n|$)/g,
- ),
- ]
-
- if (matches.length === 0) {
- return {
- success: false,
- error: `Invalid diff format - missing required sections\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH\\n:start_line: start line\\n-------\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include start_line/SEARCH/=======/REPLACE sections with correct markers on new lines`,
- }
- }
-
- // Detect line ending from original content
- const lineEnding = originalContent.includes("\r\n") ? "\r\n" : "\n"
- let resultLines = originalContent.split(/\r?\n/)
- let delta = 0
- let diffResults: DiffResult[] = []
- let appliedCount = 0
-
- const replacements = matches
- .map((match) => ({
- startLine: _paramStartLine ?? Number(match[2] ?? 0),
- searchContent: match[6],
- replaceContent: match[7],
- }))
- .sort((a, b) => a.startLine - b.startLine)
-
- for (const replacement of replacements) {
- let { searchContent, replaceContent } = replacement
- let startLine = replacement.startLine + (replacement.startLine === 0 ? 0 : delta)
-
- // First unescape any escaped markers in the content
- searchContent = this.unescapeMarkers(searchContent)
- replaceContent = this.unescapeMarkers(replaceContent)
-
- // Strip line numbers from search and replace content if every line starts with a line number
- const hasAllLineNumbers =
- (everyLineHasLineNumbers(searchContent) && everyLineHasLineNumbers(replaceContent)) ||
- (everyLineHasLineNumbers(searchContent) && replaceContent.trim() === "")
-
- if (hasAllLineNumbers && startLine === 0) {
- startLine = parseInt(searchContent.split("\n")[0].split("|")[0])
- }
-
- if (hasAllLineNumbers) {
- searchContent = stripLineNumbers(searchContent)
- replaceContent = stripLineNumbers(replaceContent)
- }
-
- // Validate that search and replace content are not identical
- if (searchContent === replaceContent) {
- diffResults.push({
- success: false,
- error:
- `Search and replace content are identical - no changes would be made\n\n` +
- `Debug Info:\n` +
- `- Search and replace must be different to make changes\n` +
- `- Use read_file to verify the content you want to change`,
- })
- continue
- }
-
- // Split content into lines, handling both \n and \r\n
- let searchLines = searchContent === "" ? [] : searchContent.split(/\r?\n/)
- let replaceLines = replaceContent === "" ? [] : replaceContent.split(/\r?\n/)
-
- // Validate that search content is not empty
- if (searchLines.length === 0) {
- diffResults.push({
- success: false,
- error: `Empty search content is not allowed\n\nDebug Info:\n- Search content cannot be empty\n- For insertions, provide a specific line using :start_line: and include content to search for\n- For example, match a single line to insert before/after it`,
- })
- continue
- }
-
- let endLine = replacement.startLine + searchLines.length - 1
-
- // Initialize search variables
- let matchIndex = -1
- let bestMatchScore = 0
- let bestMatchContent = ""
- let searchChunk = searchLines.join("\n")
-
- // Determine search bounds
- let searchStartIndex = 0
- let searchEndIndex = resultLines.length
-
- // Validate and handle line range if provided
- if (startLine) {
- // Convert to 0-based index
- const exactStartIndex = startLine - 1
- const searchLen = searchLines.length
- const exactEndIndex = exactStartIndex + searchLen - 1
-
- // Try exact match first
- const originalChunk = resultLines.slice(exactStartIndex, exactEndIndex + 1).join("\n")
- const similarity = getSimilarity(originalChunk, searchChunk)
-
- if (similarity >= this.fuzzyThreshold) {
- matchIndex = exactStartIndex
- bestMatchScore = similarity
- bestMatchContent = originalChunk
- } else {
- // Set bounds for buffered search
- searchStartIndex = Math.max(0, startLine - (this.bufferLines + 1))
- searchEndIndex = Math.min(resultLines.length, startLine + searchLines.length + this.bufferLines)
- }
- }
-
- // If no match found yet, try middle-out search within bounds
- if (matchIndex === -1) {
- const {
- bestScore,
- bestMatchIndex,
- bestMatchContent: midContent,
- } = fuzzySearch(resultLines, searchChunk, searchStartIndex, searchEndIndex)
-
- matchIndex = bestMatchIndex
- bestMatchScore = bestScore
- bestMatchContent = midContent
- }
-
- // Try aggressive line number stripping as a fallback if regular matching fails
- if (matchIndex === -1 || bestMatchScore < this.fuzzyThreshold) {
- // Strip both search and replace content once (simultaneously)
- const aggressiveSearchContent = stripLineNumbers(searchContent, true)
- const aggressiveReplaceContent = stripLineNumbers(replaceContent, true)
- const aggressiveSearchLines = aggressiveSearchContent ? aggressiveSearchContent.split(/\r?\n/) : []
- const aggressiveSearchChunk = aggressiveSearchLines.join("\n")
-
- // Try middle-out search again with aggressive stripped content (respecting the same search bounds)
- const {
- bestScore,
- bestMatchIndex,
- bestMatchContent: aggContent,
- } = fuzzySearch(resultLines, aggressiveSearchChunk, searchStartIndex, searchEndIndex)
-
- if (bestMatchIndex !== -1 && bestScore >= this.fuzzyThreshold) {
- matchIndex = bestMatchIndex
- bestMatchScore = bestScore
- bestMatchContent = aggContent
-
- // Replace the original search/replace with their stripped versions
- searchContent = aggressiveSearchContent
- replaceContent = aggressiveReplaceContent
- searchLines = aggressiveSearchLines
- replaceLines = replaceContent ? replaceContent.split(/\r?\n/) : []
- } else {
- // No match found with either method
- const originalContentSection =
- startLine !== undefined && endLine !== undefined
- ? `\n\nOriginal Content:\n${addLineNumbers(
- resultLines
- .slice(
- Math.max(0, startLine - 1 - this.bufferLines),
- Math.min(resultLines.length, endLine + this.bufferLines),
- )
- .join("\n"),
- Math.max(1, startLine - this.bufferLines),
- )}`
- : `\n\nOriginal Content:\n${addLineNumbers(resultLines.join("\n"))}`
-
- const bestMatchSection = bestMatchContent
- ? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}`
- : `\n\nBest Match Found:\n(no match)`
-
- const lineRange = startLine ? ` at line: ${startLine}` : ""
-
- diffResults.push({
- success: false,
- error: `No sufficiently similar match found${lineRange} (${Math.floor(
- bestMatchScore * 100,
- )}% similar, needs ${Math.floor(
- this.fuzzyThreshold * 100,
- )}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(
- bestMatchScore * 100,
- )}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${
- startLine ? `starting at line ${startLine}` : "start to end"
- }\n- Tried both standard and aggressive line number stripping\n- Tip: Use the read_file tool to get the latest content of the file before attempting to use the apply_diff tool again, as the file content may have changed\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`,
- })
- continue
- }
- }
-
- // Get the matched lines from the original content
- const matchedLines = resultLines.slice(matchIndex, matchIndex + searchLines.length)
-
- // Get the exact indentation (preserving tabs/spaces) of each line
- const originalIndents = matchedLines.map((line) => {
- const match = line.match(/^[\t ]*/)
- return match ? match[0] : ""
- })
-
- // Get the exact indentation of each line in the search block
- const searchIndents = searchLines.map((line) => {
- const match = line.match(/^[\t ]*/)
- return match ? match[0] : ""
- })
-
- // Apply the replacement while preserving exact indentation
- const indentedReplaceLines = replaceLines.map((line) => {
- // Get the matched line's exact indentation
- const matchedIndent = originalIndents[0] || ""
-
- // Get the current line's indentation relative to the search content
- const currentIndentMatch = line.match(/^[\t ]*/)
- const currentIndent = currentIndentMatch ? currentIndentMatch[0] : ""
- const searchBaseIndent = searchIndents[0] || ""
-
- // Calculate the relative indentation level
- const searchBaseLevel = searchBaseIndent.length
- const currentLevel = currentIndent.length
- const relativeLevel = currentLevel - searchBaseLevel
-
- // If relative level is negative, remove indentation from matched indent
- // If positive, add to matched indent
- const finalIndent =
- relativeLevel < 0
- ? matchedIndent.slice(0, Math.max(0, matchedIndent.length + relativeLevel))
- : matchedIndent + currentIndent.slice(searchBaseLevel)
-
- return finalIndent + line.trim()
- })
-
- // Construct the final content
- const beforeMatch = resultLines.slice(0, matchIndex)
- const afterMatch = resultLines.slice(matchIndex + searchLines.length)
- resultLines = [...beforeMatch, ...indentedReplaceLines, ...afterMatch]
-
- delta = delta - matchedLines.length + replaceLines.length
- appliedCount++
- }
-
- const finalContent = resultLines.join(lineEnding)
-
- if (appliedCount === 0) {
- return {
- success: false,
- failParts: diffResults,
- }
- }
-
- return {
- success: true,
- content: finalContent,
- failParts: diffResults,
- }
- }
-
- getProgressStatus(toolUse: ToolUse, result?: DiffResult): ToolProgressStatus {
- const diffContent = toolUse.params.diff
- if (diffContent) {
- const icon = "diff-multiple"
-
- if (toolUse.partial) {
- if (Math.floor(diffContent.length / 10) % 10 === 0) {
- const searchBlockCount = (diffContent.match(/SEARCH/g) || []).length
- return { icon, text: `${searchBlockCount}` }
- }
- } else if (result) {
- const searchBlockCount = (diffContent.match(/SEARCH/g) || []).length
- if (result.failParts?.length) {
- return {
- icon,
- text: `${searchBlockCount - result.failParts.length}/${searchBlockCount}`,
- }
- } else {
- return { icon, text: `${searchBlockCount}` }
- }
- }
- }
-
- return {}
- }
-}
diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts
index a6a9913203..f43bbee0dc 100644
--- a/src/core/diff/strategies/multi-search-replace.ts
+++ b/src/core/diff/strategies/multi-search-replace.ts
@@ -1,5 +1,3 @@
-/* eslint-disable no-irregular-whitespace */
-
import { distance } from "fastest-levenshtein"
import { ToolProgressStatus } from "@roo-code/types"
@@ -90,96 +88,6 @@ export class MultiSearchReplaceDiffStrategy implements DiffStrategy {
this.bufferLines = bufferLines ?? BUFFER_LINES
}
- getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string {
- return `## apply_diff
-Description: Request to apply PRECISE, TARGETED modifications to an existing file by searching for specific sections of content and replacing them. This tool is for SURGICAL EDITS ONLY - specific changes to existing code.
-You can perform multiple distinct search and replace operations within a single \`apply_diff\` call by providing multiple SEARCH/REPLACE blocks in the \`diff\` parameter. This is the preferred way to make several targeted changes efficiently.
-The SEARCH section must exactly match existing content including whitespace and indentation.
-If you're not confident in the exact content to search for, use the read_file tool first to get the exact content.
-When applying the diffs, be extra careful to remember to change any closing brackets or other syntax that may be affected by the diff farther down in the file.
-ALWAYS make as many changes in a single 'apply_diff' request as possible using multiple SEARCH/REPLACE blocks
-
-Parameters:
-- path: (required) The path of the file to modify (relative to the current workspace directory ${args.cwd})
-- diff: (required) The search/replace block defining the changes.
-
-Diff format:
-\`\`\`
-<<<<<<< SEARCH
-:start_line: (required) The line number of original content where the search block starts.
--------
-[exact content to find including whitespace]
-=======
-[new content to replace with]
->>>>>>> REPLACE
-
-\`\`\`
-
-
-Example:
-
-Original file:
-\`\`\`
-1 | def calculate_total(items):
-2 | total = 0
-3 | for item in items:
-4 | total += item
-5 | return total
-\`\`\`
-
-Search/Replace content:
-\`\`\`
-<<<<<<< SEARCH
-:start_line:1
--------
-def calculate_total(items):
- total = 0
- for item in items:
- total += item
- return total
-=======
-def calculate_total(items):
- """Calculate total with 10% markup"""
- return sum(item * 1.1 for item in items)
->>>>>>> REPLACE
-
-\`\`\`
-
-Search/Replace content with multiple edits:
-\`\`\`
-<<<<<<< SEARCH
-:start_line:1
--------
-def calculate_total(items):
- sum = 0
-=======
-def calculate_sum(items):
- sum = 0
->>>>>>> REPLACE
-
-<<<<<<< SEARCH
-:start_line:4
--------
- total += item
- return total
-=======
- sum += item
- return sum
->>>>>>> REPLACE
-\`\`\`
-
-
-Usage:
-
-File path here
-
-Your search/replace content here
-You can use multi search/replace block in one diff block, but make sure to include the line numbers for each block.
-Only use a single line of '=======' between search and replacement content, because multiple '=======' will corrupt the file.
-
-`
- }
-
private unescapeMarkers(content: string): string {
return content
.replace(/^\\<<<<<<>>>>>> REPLACE)(?=\n|$)
- Matches the final “>>>>>>> REPLACE” marker on its own line (and requires a following newline or the end of file).
+ Matches the final ">>>>>>> REPLACE" marker on its own line (and requires a following newline or the end of file).
*/
let matches = [
diff --git a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts
index 65b447ff16..74e000d36a 100644
--- a/src/core/environment/__tests__/getEnvironmentDetails.spec.ts
+++ b/src/core/environment/__tests__/getEnvironmentDetails.spec.ts
@@ -5,7 +5,6 @@ import delay from "delay"
import type { Mock } from "vitest"
import { getEnvironmentDetails } from "../getEnvironmentDetails"
-import { EXPERIMENT_IDS, experiments } from "../../../shared/experiments"
import { getFullModeDetails } from "../../../shared/modes"
import { isToolAllowedForMode } from "../../tools/validateToolUse"
import { getApiMetrics } from "../../../shared/getApiMetrics"
@@ -43,7 +42,6 @@ vi.mock("execa", () => ({
execa: vi.fn(),
}))
-vi.mock("../../../shared/experiments")
vi.mock("../../../shared/modes")
vi.mock("../../../shared/getApiMetrics")
vi.mock("../../../services/glob/list-files")
@@ -115,7 +113,6 @@ describe("getEnvironmentDetails", () => {
createMessage: vi.fn(),
countTokens: vi.fn(),
} as unknown as ApiHandler,
- diffEnabled: true,
providerRef: {
deref: vi.fn().mockReturnValue(mockProvider),
[Symbol.toStringTag]: "WeakRef",
@@ -322,16 +319,6 @@ describe("getEnvironmentDetails", () => {
expect(mockInactiveTerminal.getCurrentWorkingDirectory).toHaveBeenCalled()
})
- it("should include experiment-specific details when Power Steering is enabled", async () => {
- mockState.experiments = { [EXPERIMENT_IDS.POWER_STEERING]: true }
- ;(experiments.isEnabled as Mock).mockReturnValue(true)
-
- const result = await getEnvironmentDetails(mockCline as Task)
-
- expect(result).toContain("You are a code assistant")
- expect(result).toContain("Custom instructions")
- })
-
it("should handle missing provider or state", async () => {
// Mock provider to return null.
mockCline.providerRef!.deref = vi.fn().mockReturnValue(null)
diff --git a/src/core/environment/getEnvironmentDetails.ts b/src/core/environment/getEnvironmentDetails.ts
index ebb6f18e48..db5a0cd088 100644
--- a/src/core/environment/getEnvironmentDetails.ts
+++ b/src/core/environment/getEnvironmentDetails.ts
@@ -6,10 +6,7 @@ import pWaitFor from "p-wait-for"
import delay from "delay"
import type { ExperimentId } from "@roo-code/types"
-import { DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT } from "@roo-code/types"
-import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
-import { EXPERIMENT_IDS, experiments as Experiments } from "../../shared/experiments"
import { formatLanguage } from "../../shared/language"
import { defaultModeSlug, getFullModeDetails } from "../../shared/modes"
import { getApiMetrics } from "../../shared/getApiMetrics"
@@ -28,11 +25,7 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
const clineProvider = cline.providerRef.deref()
const state = await clineProvider?.getState()
- const {
- terminalOutputLineLimit = 500,
- terminalOutputCharacterLimit = DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
- maxWorkspaceFiles = 200,
- } = state ?? {}
+ const { maxWorkspaceFiles = 200 } = state ?? {}
// It could be useful for cline to know if the user went from one or no
// file to another between messages, so we always include this context.
@@ -114,11 +107,7 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
let newOutput = TerminalRegistry.getUnretrievedOutput(busyTerminal.id)
if (newOutput) {
- newOutput = Terminal.compressTerminalOutput(
- newOutput,
- terminalOutputLineLimit,
- terminalOutputCharacterLimit,
- )
+ newOutput = Terminal.compressTerminalOutput(newOutput)
terminalDetails += `\n### New Output\n${newOutput}`
}
}
@@ -146,11 +135,7 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
let output = process.getUnretrievedOutput()
if (output) {
- output = Terminal.compressTerminalOutput(
- output,
- terminalOutputLineLimit,
- terminalOutputCharacterLimit,
- )
+ output = Terminal.compressTerminalOutput(output)
terminalOutputs.push(`Command: \`${process.command}\`\n${output}`)
}
}
@@ -236,26 +221,13 @@ export async function getEnvironmentDetails(cline: Task, includeFileDetails: boo
language: language ?? formatLanguage(vscode.env.language),
})
- // Use the task's locked tool protocol for consistent environment details.
- // This ensures the model sees the same tool format it was started with,
- // even if user settings have changed. Fall back to resolving fresh if
- // the task hasn't been fully initialized yet (shouldn't happen in practice).
- const modelInfo = cline.api.getModel().info
- const toolProtocol = resolveToolProtocol(state?.apiConfiguration ?? {}, modelInfo, cline.taskToolProtocol)
+ const toolFormat = "native"
details += `\n\n# Current Mode\n`
details += `${currentMode}\n`
details += `${modeDetails.name}\n`
details += `${modelId}\n`
- details += `${toolProtocol}\n`
-
- if (Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.POWER_STEERING)) {
- details += `${modeDetails.roleDefinition}\n`
-
- if (modeDetails.customInstructions) {
- details += `${modeDetails.customInstructions}\n`
- }
- }
+ details += `${toolFormat}\n`
// Add browser session status - Only show when active to prevent cluttering context
const isBrowserActive = cline.browserSession.isSessionActive()
diff --git a/src/core/mentions/__tests__/processUserContentMentions.spec.ts b/src/core/mentions/__tests__/processUserContentMentions.spec.ts
index ec2e08f92a..4f45e404cc 100644
--- a/src/core/mentions/__tests__/processUserContentMentions.spec.ts
+++ b/src/core/mentions/__tests__/processUserContentMentions.spec.ts
@@ -34,7 +34,7 @@ describe("processUserContentMentions", () => {
const userContent = [
{
type: "text" as const,
- text: "Read file with limit",
+ text: "Read file with limit",
},
]
@@ -48,7 +48,7 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).toHaveBeenCalledWith(
- "Read file with limit",
+ "Read file with limit",
"/test",
mockUrlContentFetcher,
mockFileContextTracker,
@@ -64,7 +64,7 @@ describe("processUserContentMentions", () => {
const userContent = [
{
type: "text" as const,
- text: "Read file without limit",
+ text: "Read file without limit",
},
]
@@ -77,7 +77,7 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).toHaveBeenCalledWith(
- "Read file without limit",
+ "Read file without limit",
"/test",
mockUrlContentFetcher,
mockFileContextTracker,
@@ -93,7 +93,7 @@ describe("processUserContentMentions", () => {
const userContent = [
{
type: "text" as const,
- text: "Read unlimited lines",
+ text: "Read unlimited lines",
},
]
@@ -107,7 +107,7 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).toHaveBeenCalledWith(
- "Read unlimited lines",
+ "Read unlimited lines",
"/test",
mockUrlContentFetcher,
mockFileContextTracker,
@@ -121,11 +121,11 @@ describe("processUserContentMentions", () => {
})
describe("content processing", () => {
- it("should process text blocks with tags", async () => {
+ it("should process text blocks with tags", async () => {
const userContent = [
{
type: "text" as const,
- text: "Do something",
+ text: "Do something",
},
]
@@ -139,35 +139,12 @@ describe("processUserContentMentions", () => {
expect(parseMentions).toHaveBeenCalled()
expect(result.content[0]).toEqual({
type: "text",
- text: "parsed: Do something",
+ text: "parsed: Do something",
})
expect(result.mode).toBeUndefined()
})
- it("should process text blocks with tags", async () => {
- const userContent = [
- {
- type: "text" as const,
- text: "Fix this issue",
- },
- ]
-
- const result = await processUserContentMentions({
- userContent,
- cwd: "/test",
- urlContentFetcher: mockUrlContentFetcher,
- fileContextTracker: mockFileContextTracker,
- })
-
- expect(parseMentions).toHaveBeenCalled()
- expect(result.content[0]).toEqual({
- type: "text",
- text: "parsed: Fix this issue",
- })
- expect(result.mode).toBeUndefined()
- })
-
- it("should not process text blocks without task or feedback tags", async () => {
+ it("should not process text blocks without user_message tags", async () => {
const userContent = [
{
type: "text" as const,
@@ -192,7 +169,7 @@ describe("processUserContentMentions", () => {
{
type: "tool_result" as const,
tool_use_id: "123",
- content: "Tool feedback",
+ content: "Tool feedback",
},
]
@@ -207,7 +184,7 @@ describe("processUserContentMentions", () => {
expect(result.content[0]).toEqual({
type: "tool_result",
tool_use_id: "123",
- content: "parsed: Tool feedback",
+ content: "parsed: Tool feedback",
})
expect(result.mode).toBeUndefined()
})
@@ -220,7 +197,7 @@ describe("processUserContentMentions", () => {
content: [
{
type: "text" as const,
- text: "Array task",
+ text: "Array task",
},
{
type: "text" as const,
@@ -244,7 +221,7 @@ describe("processUserContentMentions", () => {
content: [
{
type: "text",
- text: "parsed: Array task",
+ text: "parsed: Array task",
},
{
type: "text",
@@ -259,7 +236,7 @@ describe("processUserContentMentions", () => {
const userContent = [
{
type: "text" as const,
- text: "First task",
+ text: "First task",
},
{
type: "image" as const,
@@ -272,7 +249,7 @@ describe("processUserContentMentions", () => {
{
type: "tool_result" as const,
tool_use_id: "456",
- content: "Feedback",
+ content: "Feedback",
},
]
@@ -288,13 +265,13 @@ describe("processUserContentMentions", () => {
expect(result.content).toHaveLength(3)
expect(result.content[0]).toEqual({
type: "text",
- text: "parsed: First task",
+ text: "parsed: First task",
})
expect(result.content[1]).toEqual(userContent[1]) // Image block unchanged
expect(result.content[2]).toEqual({
type: "tool_result",
tool_use_id: "456",
- content: "parsed: Feedback",
+ content: "parsed: Feedback",
})
expect(result.mode).toBeUndefined()
})
@@ -305,7 +282,7 @@ describe("processUserContentMentions", () => {
const userContent = [
{
type: "text" as const,
- text: "Test default",
+ text: "Test default",
},
]
@@ -317,7 +294,7 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).toHaveBeenCalledWith(
- "Test default",
+ "Test default",
"/test",
mockUrlContentFetcher,
mockFileContextTracker,
@@ -333,7 +310,7 @@ describe("processUserContentMentions", () => {
const userContent = [
{
type: "text" as const,
- text: "Test explicit false",
+ text: "Test explicit false",
},
]
@@ -346,7 +323,7 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).toHaveBeenCalledWith(
- "Test explicit false",
+ "Test explicit false",
"/test",
mockUrlContentFetcher,
mockFileContextTracker,
@@ -358,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: "Run command",
+ },
+ ]
+
+ 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: "Tool output",
+ },
+ ]
+
+ 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: "Array item",
+ },
+ ],
+ },
+ ]
+
+ 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",
+ },
+ ],
+ })
+ })
+ })
})
diff --git a/src/core/mentions/index.ts b/src/core/mentions/index.ts
index 2bbbf9ed0d..ebff1bcd8c 100644
--- a/src/core/mentions/index.ts
+++ b/src/core/mentions/index.ts
@@ -73,6 +73,7 @@ export async function openMention(cwd: string, mention?: string): Promise
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\n${commandOutput}\n`
+ slashCommandHelp += `\n\n\n${commandOutput}\n`
} catch (error) {
- parsedText += `\n\n\nError loading command '${commandName}': ${error.message}\n`
+ slashCommandHelp += `\n\n\nError loading command '${commandName}': ${error.message}\n`
}
}
@@ -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(
diff --git a/src/core/mentions/processUserContentMentions.ts b/src/core/mentions/processUserContentMentions.ts
index 5ea78f4dc3..79911adcb9 100644
--- a/src/core/mentions/processUserContentMentions.ts
+++ b/src/core/mentions/processUserContentMentions.ts
@@ -38,50 +38,19 @@ export async function processUserContentMentions({
// Process userContent array, which contains various block types:
// TextBlockParam, ImageBlockParam, ToolUseBlockParam, and ToolResultBlockParam.
// We need to apply parseMentions() to:
- // 1. All TextBlockParam's text (first user message with task)
+ // 1. All TextBlockParam's text (first user message)
// 2. ToolResultBlockParam's content/context text arrays if it contains
- // "" (see formatToolDeniedFeedback, attemptCompletion,
- // executeCommand, and consecutiveMistakeCount >= 3) or ""
- // (see askFollowupQuestion), we place all user generated content in
- // these tags so they can effectively be used as markers for when we
- // should parse mentions).
- const content = await Promise.all(
- userContent.map(async (block) => {
- const shouldProcessMentions = (text: string) =>
- text.includes("") ||
- text.includes("") ||
- text.includes("") ||
- text.includes("")
+ // "" - 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("")
- 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,
@@ -95,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 }
}
diff --git a/src/core/message-manager/index.spec.ts b/src/core/message-manager/index.spec.ts
index e2c11db3b7..3fd99793bf 100644
--- a/src/core/message-manager/index.spec.ts
+++ b/src/core/message-manager/index.spec.ts
@@ -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,
diff --git a/src/core/message-manager/index.ts b/src/core/message-manager/index.ts
index e35f290c39..4b68be0825 100644
--- a/src/core/message-manager/index.ts
+++ b/src/core/message-manager/index.ts
@@ -1,7 +1,10 @@
+import * as path from "path"
import { Task } from "../task/Task"
import { ClineMessage } from "@roo-code/types"
import { ApiMessage } from "../task-persistence/apiMessages"
import { cleanupAfterTruncation } from "../condense"
+import { OutputInterceptor } from "../../integrations/terminal/OutputInterceptor"
+import { getTaskDirectoryPath } from "../../utils/storage"
export interface RewindOptions {
/** Whether to include the target message in deletion (edit=true, delete=false) */
@@ -207,6 +210,32 @@ export class MessageManager {
apiHistory = cleanupAfterTruncation(apiHistory)
}
+ // Step 6: Cleanup orphaned command output artifacts
+ // Collect timestamps from remaining messages to identify valid artifact IDs
+ // Artifacts whose IDs don't match any remaining message timestamp will be removed
+ if (!skipCleanup) {
+ const validIds = new Set()
+
+ // Collect timestamps from remaining clineMessages
+ for (const msg of this.task.clineMessages) {
+ if (msg.ts) {
+ validIds.add(String(msg.ts))
+ }
+ }
+
+ // Collect timestamps from remaining apiHistory
+ for (const msg of apiHistory) {
+ if (msg.ts) {
+ validIds.add(String(msg.ts))
+ }
+ }
+
+ // Cleanup artifacts asynchronously (fire-and-forget with error handling)
+ this.cleanupOrphanedArtifacts(validIds).catch((error) => {
+ console.error("[MessageManager] Error cleaning up orphaned command output artifacts:", error)
+ })
+ }
+
// Only write if the history actually changed
const historyChanged =
apiHistory.length !== originalHistory.length || apiHistory.some((msg, i) => msg !== originalHistory[i])
@@ -215,4 +244,28 @@ export class MessageManager {
await this.task.overwriteApiConversationHistory(apiHistory)
}
}
+
+ /**
+ * Cleanup orphaned command output artifacts.
+ * Removes artifact files whose execution IDs don't match any remaining message timestamps.
+ */
+ private async cleanupOrphanedArtifacts(validIds: Set): Promise {
+ try {
+ // Access globalStoragePath and taskId through the task reference
+ const task = this.task as any // Access private member
+ const globalStoragePath = task.globalStoragePath
+ const taskId = task.taskId
+
+ if (!globalStoragePath || !taskId) {
+ return
+ }
+
+ const taskDir = await getTaskDirectoryPath(globalStoragePath, taskId)
+ const outputDir = path.join(taskDir, "command-output")
+ await OutputInterceptor.cleanupByIds(outputDir, validIds)
+ } catch (error) {
+ // Silently fail - cleanup is best-effort
+ console.debug("[MessageManager] Artifact cleanup skipped:", error)
+ }
+ }
}
diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap
index ee8a50e993..5bed6df09d 100644
--- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap
+++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/architect-mode-prompt.snap
@@ -10,361 +10,15 @@ ALL responses MUST show ANY `language construct` OR filename reference as clicka
TOOL USE
-You have access to a set of tools that are executed upon the user's approval. You must use exactly one tool per message, and every assistant message must include a tool call. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
+You have access to a set of tools that are executed upon the user's approval. Use the provider-native tool-calling mechanism. Do not include XML markup or examples. You must call at least one tool per assistant response. Prefer calling as many tools as are reasonably needed in a single response to reduce back-and-forth and complete tasks faster.
-# Tool Use Formatting
-
-Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:
-
-
-value1
-value2
-...
-
-
-Always use the actual tool name as the XML tag name for proper parsing and execution.
-
-# Tools
-
-## read_file
-Description: Request to read the contents of one or more files. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when creating diffs or discussing code. Supports text extraction from PDF and DOCX files, but may not handle other binary files properly.
-
-**IMPORTANT: You can read a maximum of 5 files in a single request.** If you need to read more files, use multiple sequential read_file requests.
-
-
-Parameters:
-- args: Contains one or more file elements, where each file contains:
- - path: (required) File path (relative to workspace directory /test/path)
-
-
-Usage:
-
-
-
- path/to/file
-
-
-
-
-
-Examples:
-
-1. Reading a single file:
-
-
-
- src/app.ts
-
-
-
-
-
-2. Reading multiple files (within the 5-file limit):
-
-
-
- src/app.ts
-
-
-
- src/utils.ts
-
-
-
-
-
-3. Reading an entire file:
-
-
-
- config.json
-
-
-
-
-IMPORTANT: You MUST use this Efficient Reading Strategy:
-- You MUST read all related files and implementations together in a single operation (up to 5 files at once)
-- You MUST obtain all necessary context before proceeding with changes
-
-- When you need to read more than 5 files, prioritize the most critical files first, then use subsequent read_file requests for additional files
-
-## fetch_instructions
-Description: Request to fetch instructions to perform a task
-Parameters:
-- task: (required) The task to get instructions for. This can take the following values:
- create_mcp_server
- create_mode
-
-Example: Requesting instructions to create an MCP Server
-
-
-create_mcp_server
-
-
-## search_files
-Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
-
-Craft your regex patterns carefully to balance specificity and flexibility. Use this tool to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include surrounding context, so analyze the surrounding code to better understand the matches. Leverage this tool in combination with other tools for more comprehensive analysis - for example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches.
-
-Parameters:
-- path: (required) The path of the directory to search in (relative to the current workspace directory /test/path). This directory will be recursively searched.
-- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax.
-- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*).
-
-Usage:
-
-Directory path here
-Your regex pattern here
-file pattern here (optional)
-
-
-Example: Searching for all .ts files in the current directory
-
-.
-.*
-*.ts
-
-
-Example: Searching for function definitions in JavaScript files
-
-src
-function\s+\w+
-*.js
-
-
-## list_files
-Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.
-Parameters:
-- path: (required) The path of the directory to list contents for (relative to the current workspace directory /test/path)
-- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
-Usage:
-
-Directory path here
-true or false (optional)
-
-
-Example: Requesting to list all files in the current directory
-
-.
-false
-
-
-## write_to_file
-Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
-
-**Important:** You should prefer using other editing tools over write_to_file when making changes to existing files, since write_to_file is slower and cannot handle large files. Use write_to_file primarily for new file creation.
-
-When using this tool, use it directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code.
-
-When creating a new project, organize all new files within a dedicated project directory unless the user specifies otherwise. Structure the project logically, adhering to best practices for the specific type of project being created.
-
-Parameters:
-- path: (required) The path of the file to write to (relative to the current workspace directory /test/path)
-- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content.
-
-Usage:
-
-File path here
-
-Your file content here
-
-
-
-Example: Writing a configuration file
-
-frontend-config.json
-
-{
- "apiEndpoint": "https://api.example.com",
- "theme": {
- "primaryColor": "#007bff",
- "secondaryColor": "#6c757d",
- "fontFamily": "Arial, sans-serif"
- },
- "features": {
- "darkMode": true,
- "notifications": true,
- "analytics": false
- },
- "version": "1.0.0"
-}
-
-
-
-## ask_followup_question
-Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
-
-Parameters:
-- question: (required) A clear, specific question addressing the information needed
-- follow_up: (required) A list of 2-4 suggested answers, each in its own tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
-
-Usage:
-
-Your question here
-
-First suggestion
-Action with mode switch
-
-
-
-Example:
-
-What is the path to the frontend-config.json file?
-
-./src/frontend-config.json
-./config/frontend-config.json
-./frontend-config.json
-
-
-
-## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
-IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must confirm that you've received successful results from the user for any previous tool uses. If not, then DO NOT use this tool.
-Parameters:
-- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-Usage:
-
-
-Your final result description here
-
-
-
-Example: Requesting to attempt completion with a result
-
-
-I've updated the CSS
-
-
-
-## switch_mode
-Description: Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Code mode to make code changes. The user must approve the mode switch.
-Parameters:
-- mode_slug: (required) The slug of the mode to switch to (e.g., "code", "ask", "architect")
-- reason: (optional) The reason for switching modes
-Usage:
-
-Mode slug here
-Reason for switching here
-
-
-Example: Requesting to switch to code mode
-
-code
-Need to make code changes
-
-
-## new_task
-Description: This will let you create a new task instance in the chosen mode using your provided message.
-
-Parameters:
-- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
-- message: (required) The initial user message or instructions for this new task.
-
-Usage:
-
-your-mode-slug-here
-Your initial instructions here
-
-
-Example:
-
-code
-Implement a new feature for the application
-
-
-
-## update_todo_list
-
-**Description:**
-Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
-
-**Checklist Format:**
-- Use a single-level markdown checklist (no nesting or subtasks).
-- List todos in the intended execution order.
-- Status options:
- - [ ] Task description (pending)
- - [x] Task description (completed)
- - [-] Task description (in progress)
-
-**Status Rules:**
-- [ ] = pending (not started)
-- [x] = completed (fully finished, no unresolved issues)
-- [-] = in_progress (currently being worked on)
-
-**Core Principles:**
-- Before updating, always confirm which todos have been completed since the last update.
-- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
-- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
-- Do not remove any unfinished todos unless explicitly instructed.
-- Always retain all unfinished tasks, updating their status as needed.
-- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
-- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
-- Remove tasks only if they are no longer relevant or if the user requests deletion.
-
-**Usage Example:**
-
-
-[x] Analyze requirements
-[x] Design architecture
-[-] Implement core logic
-[ ] Write tests
-[ ] Update documentation
-
-
-
-*After completing "Implement core logic" and starting "Write tests":*
-
-
-[x] Analyze requirements
-[x] Design architecture
-[x] Implement core logic
-[-] Write tests
-[ ] Update documentation
-[ ] Add performance benchmarks
-
-
-
-**When to Use:**
-- The task is complicated or involves multiple steps or requires ongoing tracking.
-- You need to update the status of several todos at once.
-- New actionable items are discovered during task execution.
-- The user requests a todo list or provides multiple tasks.
-- The task is complex and benefits from clear, stepwise progress tracking.
-
-**When NOT to Use:**
-- There is only a single, trivial task.
-- The task can be completed in one or two simple steps.
-- The request is purely conversational or informational.
-
-**Task Management Guidelines:**
-- Mark task as completed immediately after all work of the current task is done.
-- Start the next task by marking it as in_progress.
-- Add new todos as soon as they are identified.
-- Use clear, descriptive task names.
-
-
-# Tool Use Guidelines
+ # Tool Use Guidelines
1. Assess what information you already have and what information you need to proceed with the task.
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like `ls` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
-3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
-4. Formulate your tool use using the XML format specified for each tool.
-5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
- - Information about whether the tool succeeded or failed, along with any reasons for failure.
- - Linter errors that may have arisen due to the changes you made, which you'll need to address.
- - New terminal output in reaction to the changes, which you may need to consider or act upon.
- - Any other relevant feedback or information related to the tool use.
-6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
-
-It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:
-1. Confirm the success of each step before proceeding.
-2. Address any issues or errors that arise immediately.
-3. Adapt your approach based on new information or unexpected results.
-4. Ensure that each action builds correctly on the previous ones.
-
-By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
-
+3. If multiple actions are needed, you may use multiple tools in a single message when appropriate, or use tools iteratively across messages. Each tool use should be informed by the results of previous tool uses. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
+By carefully considering the user's response after tool executions, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
====
@@ -385,7 +39,7 @@ MODES
RULES
- The project base directory is: /test/path
-- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to .
+- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to execute_command.
- You cannot `cd` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path.
- Do not use the ~ character or $HOME to refer to the home directory.
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap
index 4428748632..243dfc19b7 100644
--- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap
+++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/ask-mode-prompt.snap
@@ -10,319 +10,15 @@ ALL responses MUST show ANY `language construct` OR filename reference as clicka
TOOL USE
-You have access to a set of tools that are executed upon the user's approval. You must use exactly one tool per message, and every assistant message must include a tool call. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
+You have access to a set of tools that are executed upon the user's approval. Use the provider-native tool-calling mechanism. Do not include XML markup or examples. You must call at least one tool per assistant response. Prefer calling as many tools as are reasonably needed in a single response to reduce back-and-forth and complete tasks faster.
-# Tool Use Formatting
-
-Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:
-
-
-value1
-value2
-...
-
-
-Always use the actual tool name as the XML tag name for proper parsing and execution.
-
-# Tools
-
-## read_file
-Description: Request to read the contents of one or more files. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when creating diffs or discussing code. Supports text extraction from PDF and DOCX files, but may not handle other binary files properly.
-
-**IMPORTANT: You can read a maximum of 5 files in a single request.** If you need to read more files, use multiple sequential read_file requests.
-
-
-Parameters:
-- args: Contains one or more file elements, where each file contains:
- - path: (required) File path (relative to workspace directory /test/path)
-
-
-Usage:
-
-
-
- path/to/file
-
-
-
-
-
-Examples:
-
-1. Reading a single file:
-
-
-
- src/app.ts
-
-
-
-
-
-2. Reading multiple files (within the 5-file limit):
-
-
-
- src/app.ts
-
-
-
- src/utils.ts
-
-
-
-
-
-3. Reading an entire file:
-
-
-
- config.json
-
-
-
-
-IMPORTANT: You MUST use this Efficient Reading Strategy:
-- You MUST read all related files and implementations together in a single operation (up to 5 files at once)
-- You MUST obtain all necessary context before proceeding with changes
-
-- When you need to read more than 5 files, prioritize the most critical files first, then use subsequent read_file requests for additional files
-
-## fetch_instructions
-Description: Request to fetch instructions to perform a task
-Parameters:
-- task: (required) The task to get instructions for. This can take the following values:
- create_mcp_server
- create_mode
-
-Example: Requesting instructions to create an MCP Server
-
-
-create_mcp_server
-
-
-## search_files
-Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
-
-Craft your regex patterns carefully to balance specificity and flexibility. Use this tool to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include surrounding context, so analyze the surrounding code to better understand the matches. Leverage this tool in combination with other tools for more comprehensive analysis - for example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches.
-
-Parameters:
-- path: (required) The path of the directory to search in (relative to the current workspace directory /test/path). This directory will be recursively searched.
-- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax.
-- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*).
-
-Usage:
-
-Directory path here
-Your regex pattern here
-file pattern here (optional)
-
-
-Example: Searching for all .ts files in the current directory
-
-.
-.*
-*.ts
-
-
-Example: Searching for function definitions in JavaScript files
-
-src
-function\s+\w+
-*.js
-
-
-## list_files
-Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.
-Parameters:
-- path: (required) The path of the directory to list contents for (relative to the current workspace directory /test/path)
-- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
-Usage:
-
-Directory path here
-true or false (optional)
-
-
-Example: Requesting to list all files in the current directory
-
-.
-false
-
-
-## ask_followup_question
-Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
-
-Parameters:
-- question: (required) A clear, specific question addressing the information needed
-- follow_up: (required) A list of 2-4 suggested answers, each in its own tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
-
-Usage:
-
-Your question here
-
-First suggestion
-Action with mode switch
-
-
-
-Example:
-
-What is the path to the frontend-config.json file?
-
-./src/frontend-config.json
-./config/frontend-config.json
-./frontend-config.json
-
-
-
-## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
-IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must confirm that you've received successful results from the user for any previous tool uses. If not, then DO NOT use this tool.
-Parameters:
-- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-Usage:
-
-
-Your final result description here
-
-
-
-Example: Requesting to attempt completion with a result
-
-
-I've updated the CSS
-
-
-
-## switch_mode
-Description: Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Code mode to make code changes. The user must approve the mode switch.
-Parameters:
-- mode_slug: (required) The slug of the mode to switch to (e.g., "code", "ask", "architect")
-- reason: (optional) The reason for switching modes
-Usage:
-
-Mode slug here
-Reason for switching here
-
-
-Example: Requesting to switch to code mode
-
-code
-Need to make code changes
-
-
-## new_task
-Description: This will let you create a new task instance in the chosen mode using your provided message.
-
-Parameters:
-- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
-- message: (required) The initial user message or instructions for this new task.
-
-Usage:
-
-your-mode-slug-here
-Your initial instructions here
-
-
-Example:
-
-code
-Implement a new feature for the application
-
-
-
-## update_todo_list
-
-**Description:**
-Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
-
-**Checklist Format:**
-- Use a single-level markdown checklist (no nesting or subtasks).
-- List todos in the intended execution order.
-- Status options:
- - [ ] Task description (pending)
- - [x] Task description (completed)
- - [-] Task description (in progress)
-
-**Status Rules:**
-- [ ] = pending (not started)
-- [x] = completed (fully finished, no unresolved issues)
-- [-] = in_progress (currently being worked on)
-
-**Core Principles:**
-- Before updating, always confirm which todos have been completed since the last update.
-- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
-- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
-- Do not remove any unfinished todos unless explicitly instructed.
-- Always retain all unfinished tasks, updating their status as needed.
-- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
-- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
-- Remove tasks only if they are no longer relevant or if the user requests deletion.
-
-**Usage Example:**
-
-
-[x] Analyze requirements
-[x] Design architecture
-[-] Implement core logic
-[ ] Write tests
-[ ] Update documentation
-
-
-
-*After completing "Implement core logic" and starting "Write tests":*
-
-
-[x] Analyze requirements
-[x] Design architecture
-[x] Implement core logic
-[-] Write tests
-[ ] Update documentation
-[ ] Add performance benchmarks
-
-
-
-**When to Use:**
-- The task is complicated or involves multiple steps or requires ongoing tracking.
-- You need to update the status of several todos at once.
-- New actionable items are discovered during task execution.
-- The user requests a todo list or provides multiple tasks.
-- The task is complex and benefits from clear, stepwise progress tracking.
-
-**When NOT to Use:**
-- There is only a single, trivial task.
-- The task can be completed in one or two simple steps.
-- The request is purely conversational or informational.
-
-**Task Management Guidelines:**
-- Mark task as completed immediately after all work of the current task is done.
-- Start the next task by marking it as in_progress.
-- Add new todos as soon as they are identified.
-- Use clear, descriptive task names.
-
-
-# Tool Use Guidelines
+ # Tool Use Guidelines
1. Assess what information you already have and what information you need to proceed with the task.
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like `ls` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
-3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
-4. Formulate your tool use using the XML format specified for each tool.
-5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
- - Information about whether the tool succeeded or failed, along with any reasons for failure.
- - Linter errors that may have arisen due to the changes you made, which you'll need to address.
- - New terminal output in reaction to the changes, which you may need to consider or act upon.
- - Any other relevant feedback or information related to the tool use.
-6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
-
-It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:
-1. Confirm the success of each step before proceeding.
-2. Address any issues or errors that arise immediately.
-3. Adapt your approach based on new information or unexpected results.
-4. Ensure that each action builds correctly on the previous ones.
-
-By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
-
+3. If multiple actions are needed, you may use multiple tools in a single message when appropriate, or use tools iteratively across messages. Each tool use should be informed by the results of previous tool uses. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
+By carefully considering the user's response after tool executions, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
====
@@ -343,7 +39,7 @@ MODES
RULES
- The project base directory is: /test/path
-- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to .
+- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to execute_command.
- You cannot `cd` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path.
- Do not use the ~ character or $HOME to refer to the home directory.
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap
index 48b39d001f..5bed6df09d 100644
--- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap
+++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-disabled.snap
@@ -10,360 +10,15 @@ ALL responses MUST show ANY `language construct` OR filename reference as clicka
TOOL USE
-You have access to a set of tools that are executed upon the user's approval. You must use exactly one tool per message, and every assistant message must include a tool call. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
+You have access to a set of tools that are executed upon the user's approval. Use the provider-native tool-calling mechanism. Do not include XML markup or examples. You must call at least one tool per assistant response. Prefer calling as many tools as are reasonably needed in a single response to reduce back-and-forth and complete tasks faster.
-# Tool Use Formatting
-
-Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:
-
-
-value1
-value2
-...
-
-
-Always use the actual tool name as the XML tag name for proper parsing and execution.
-
-# Tools
-
-## read_file
-Description: Request to read the contents of one or more files. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when creating diffs or discussing code. Supports text extraction from PDF and DOCX files, but may not handle other binary files properly.
-
-**IMPORTANT: You can read a maximum of 5 files in a single request.** If you need to read more files, use multiple sequential read_file requests.
-
-
-Parameters:
-- args: Contains one or more file elements, where each file contains:
- - path: (required) File path (relative to workspace directory /test/path)
-
-
-Usage:
-
-
-
- path/to/file
-
-
-
-
-
-Examples:
-
-1. Reading a single file:
-
-
-
- src/app.ts
-
-
-
-
-
-2. Reading multiple files (within the 5-file limit):
-
-
-
- src/app.ts
-
-
-
- src/utils.ts
-
-
-
-
-
-3. Reading an entire file:
-
-
-
- config.json
-
-
-
-
-IMPORTANT: You MUST use this Efficient Reading Strategy:
-- You MUST read all related files and implementations together in a single operation (up to 5 files at once)
-- You MUST obtain all necessary context before proceeding with changes
-
-- When you need to read more than 5 files, prioritize the most critical files first, then use subsequent read_file requests for additional files
-
-## fetch_instructions
-Description: Request to fetch instructions to perform a task
-Parameters:
-- task: (required) The task to get instructions for. This can take the following values:
- create_mode
-
-Example: Requesting instructions to create a Mode
-
-
-create_mode
-
-
-## search_files
-Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
-
-Craft your regex patterns carefully to balance specificity and flexibility. Use this tool to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include surrounding context, so analyze the surrounding code to better understand the matches. Leverage this tool in combination with other tools for more comprehensive analysis - for example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches.
-
-Parameters:
-- path: (required) The path of the directory to search in (relative to the current workspace directory /test/path). This directory will be recursively searched.
-- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax.
-- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*).
-
-Usage:
-
-Directory path here
-Your regex pattern here
-file pattern here (optional)
-
-
-Example: Searching for all .ts files in the current directory
-
-.
-.*
-*.ts
-
-
-Example: Searching for function definitions in JavaScript files
-
-src
-function\s+\w+
-*.js
-
-
-## list_files
-Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.
-Parameters:
-- path: (required) The path of the directory to list contents for (relative to the current workspace directory /test/path)
-- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
-Usage:
-
-Directory path here
-true or false (optional)
-
-
-Example: Requesting to list all files in the current directory
-
-.
-false
-
-
-## write_to_file
-Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
-
-**Important:** You should prefer using other editing tools over write_to_file when making changes to existing files, since write_to_file is slower and cannot handle large files. Use write_to_file primarily for new file creation.
-
-When using this tool, use it directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code.
-
-When creating a new project, organize all new files within a dedicated project directory unless the user specifies otherwise. Structure the project logically, adhering to best practices for the specific type of project being created.
-
-Parameters:
-- path: (required) The path of the file to write to (relative to the current workspace directory /test/path)
-- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content.
-
-Usage:
-
-File path here
-
-Your file content here
-
-
-
-Example: Writing a configuration file
-
-frontend-config.json
-
-{
- "apiEndpoint": "https://api.example.com",
- "theme": {
- "primaryColor": "#007bff",
- "secondaryColor": "#6c757d",
- "fontFamily": "Arial, sans-serif"
- },
- "features": {
- "darkMode": true,
- "notifications": true,
- "analytics": false
- },
- "version": "1.0.0"
-}
-
-
-
-## ask_followup_question
-Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
-
-Parameters:
-- question: (required) A clear, specific question addressing the information needed
-- follow_up: (required) A list of 2-4 suggested answers, each in its own tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
-
-Usage:
-
-Your question here
-
-First suggestion
-Action with mode switch
-
-
-
-Example:
-
-What is the path to the frontend-config.json file?
-
-./src/frontend-config.json
-./config/frontend-config.json
-./frontend-config.json
-
-
-
-## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
-IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must confirm that you've received successful results from the user for any previous tool uses. If not, then DO NOT use this tool.
-Parameters:
-- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-Usage:
-
-
-Your final result description here
-
-
-
-Example: Requesting to attempt completion with a result
-
-
-I've updated the CSS
-
-
-
-## switch_mode
-Description: Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Code mode to make code changes. The user must approve the mode switch.
-Parameters:
-- mode_slug: (required) The slug of the mode to switch to (e.g., "code", "ask", "architect")
-- reason: (optional) The reason for switching modes
-Usage:
-
-Mode slug here
-Reason for switching here
-
-
-Example: Requesting to switch to code mode
-
-code
-Need to make code changes
-
-
-## new_task
-Description: This will let you create a new task instance in the chosen mode using your provided message.
-
-Parameters:
-- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
-- message: (required) The initial user message or instructions for this new task.
-
-Usage:
-
-your-mode-slug-here
-Your initial instructions here
-
-
-Example:
-
-code
-Implement a new feature for the application
-
-
-
-## update_todo_list
-
-**Description:**
-Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
-
-**Checklist Format:**
-- Use a single-level markdown checklist (no nesting or subtasks).
-- List todos in the intended execution order.
-- Status options:
- - [ ] Task description (pending)
- - [x] Task description (completed)
- - [-] Task description (in progress)
-
-**Status Rules:**
-- [ ] = pending (not started)
-- [x] = completed (fully finished, no unresolved issues)
-- [-] = in_progress (currently being worked on)
-
-**Core Principles:**
-- Before updating, always confirm which todos have been completed since the last update.
-- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
-- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
-- Do not remove any unfinished todos unless explicitly instructed.
-- Always retain all unfinished tasks, updating their status as needed.
-- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
-- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
-- Remove tasks only if they are no longer relevant or if the user requests deletion.
-
-**Usage Example:**
-
-
-[x] Analyze requirements
-[x] Design architecture
-[-] Implement core logic
-[ ] Write tests
-[ ] Update documentation
-
-
-
-*After completing "Implement core logic" and starting "Write tests":*
-
-
-[x] Analyze requirements
-[x] Design architecture
-[x] Implement core logic
-[-] Write tests
-[ ] Update documentation
-[ ] Add performance benchmarks
-
-
-
-**When to Use:**
-- The task is complicated or involves multiple steps or requires ongoing tracking.
-- You need to update the status of several todos at once.
-- New actionable items are discovered during task execution.
-- The user requests a todo list or provides multiple tasks.
-- The task is complex and benefits from clear, stepwise progress tracking.
-
-**When NOT to Use:**
-- There is only a single, trivial task.
-- The task can be completed in one or two simple steps.
-- The request is purely conversational or informational.
-
-**Task Management Guidelines:**
-- Mark task as completed immediately after all work of the current task is done.
-- Start the next task by marking it as in_progress.
-- Add new todos as soon as they are identified.
-- Use clear, descriptive task names.
-
-
-# Tool Use Guidelines
+ # Tool Use Guidelines
1. Assess what information you already have and what information you need to proceed with the task.
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like `ls` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
-3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
-4. Formulate your tool use using the XML format specified for each tool.
-5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
- - Information about whether the tool succeeded or failed, along with any reasons for failure.
- - Linter errors that may have arisen due to the changes you made, which you'll need to address.
- - New terminal output in reaction to the changes, which you may need to consider or act upon.
- - Any other relevant feedback or information related to the tool use.
-6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
-
-It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:
-1. Confirm the success of each step before proceeding.
-2. Address any issues or errors that arise immediately.
-3. Adapt your approach based on new information or unexpected results.
-4. Ensure that each action builds correctly on the previous ones.
-
-By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
-
+3. If multiple actions are needed, you may use multiple tools in a single message when appropriate, or use tools iteratively across messages. Each tool use should be informed by the results of previous tool uses. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
+By carefully considering the user's response after tool executions, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
====
@@ -384,7 +39,7 @@ MODES
RULES
- The project base directory is: /test/path
-- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to .
+- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to execute_command.
- You cannot `cd` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path.
- Do not use the ~ character or $HOME to refer to the home directory.
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap
deleted file mode 100644
index acc36d1ffd..0000000000
--- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap
+++ /dev/null
@@ -1,541 +0,0 @@
-You are Roo, an experienced technical leader who is inquisitive and an excellent planner. Your goal is to gather information and get context to create a detailed plan for accomplishing the user's task, which the user will review and approve before they switch into another mode to implement the solution.
-
-====
-
-MARKDOWN RULES
-
-ALL responses MUST show ANY `language construct` OR filename reference as clickable, exactly as [`filename OR language.declaration()`](relative/file/path.ext:line); line is required for `syntax` and optional for filename links. This applies to ALL markdown responses and ALSO those in attempt_completion
-
-====
-
-TOOL USE
-
-You have access to a set of tools that are executed upon the user's approval. You must use exactly one tool per message, and every assistant message must include a tool call. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
-
-# Tool Use Formatting
-
-Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:
-
-
-value1
-value2
-...
-
-
-Always use the actual tool name as the XML tag name for proper parsing and execution.
-
-# Tools
-
-## read_file
-Description: Request to read the contents of one or more files. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when creating diffs or discussing code. Supports text extraction from PDF and DOCX files, but may not handle other binary files properly.
-
-**IMPORTANT: You can read a maximum of 5 files in a single request.** If you need to read more files, use multiple sequential read_file requests.
-
-
-Parameters:
-- args: Contains one or more file elements, where each file contains:
- - path: (required) File path (relative to workspace directory /test/path)
-
-
-Usage:
-
-
-
- path/to/file
-
-
-
-
-
-Examples:
-
-1. Reading a single file:
-
-
-
- src/app.ts
-
-
-
-
-
-2. Reading multiple files (within the 5-file limit):
-
-
-
- src/app.ts
-
-
-
- src/utils.ts
-
-
-
-
-
-3. Reading an entire file:
-
-
-
- config.json
-
-
-
-
-IMPORTANT: You MUST use this Efficient Reading Strategy:
-- You MUST read all related files and implementations together in a single operation (up to 5 files at once)
-- You MUST obtain all necessary context before proceeding with changes
-
-- When you need to read more than 5 files, prioritize the most critical files first, then use subsequent read_file requests for additional files
-
-## fetch_instructions
-Description: Request to fetch instructions to perform a task
-Parameters:
-- task: (required) The task to get instructions for. This can take the following values:
- create_mcp_server
- create_mode
-
-Example: Requesting instructions to create an MCP Server
-
-
-create_mcp_server
-
-
-## search_files
-Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
-
-Craft your regex patterns carefully to balance specificity and flexibility. Use this tool to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include surrounding context, so analyze the surrounding code to better understand the matches. Leverage this tool in combination with other tools for more comprehensive analysis - for example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches.
-
-Parameters:
-- path: (required) The path of the directory to search in (relative to the current workspace directory /test/path). This directory will be recursively searched.
-- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax.
-- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*).
-
-Usage:
-
-Directory path here
-Your regex pattern here
-file pattern here (optional)
-
-
-Example: Searching for all .ts files in the current directory
-
-.
-.*
-*.ts
-
-
-Example: Searching for function definitions in JavaScript files
-
-src
-function\s+\w+
-*.js
-
-
-## list_files
-Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.
-Parameters:
-- path: (required) The path of the directory to list contents for (relative to the current workspace directory /test/path)
-- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
-Usage:
-
-Directory path here
-true or false (optional)
-
-
-Example: Requesting to list all files in the current directory
-
-.
-false
-
-
-## write_to_file
-Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
-
-**Important:** You should prefer using other editing tools over write_to_file when making changes to existing files, since write_to_file is slower and cannot handle large files. Use write_to_file primarily for new file creation.
-
-When using this tool, use it directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code.
-
-When creating a new project, organize all new files within a dedicated project directory unless the user specifies otherwise. Structure the project logically, adhering to best practices for the specific type of project being created.
-
-Parameters:
-- path: (required) The path of the file to write to (relative to the current workspace directory /test/path)
-- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content.
-
-Usage:
-
-File path here
-
-Your file content here
-
-
-
-Example: Writing a configuration file
-
-frontend-config.json
-
-{
- "apiEndpoint": "https://api.example.com",
- "theme": {
- "primaryColor": "#007bff",
- "secondaryColor": "#6c757d",
- "fontFamily": "Arial, sans-serif"
- },
- "features": {
- "darkMode": true,
- "notifications": true,
- "analytics": false
- },
- "version": "1.0.0"
-}
-
-
-
-## use_mcp_tool
-Description: Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters.
-Parameters:
-- server_name: (required) The name of the MCP server providing the tool
-- tool_name: (required) The name of the tool to execute
-- arguments: (required) A JSON object containing the tool's input parameters, following the tool's input schema
-Usage:
-
-server name here
-tool name here
-
-{
- "param1": "value1",
- "param2": "value2"
-}
-
-
-
-Example: Requesting to use an MCP tool
-
-
-weather-server
-get_forecast
-
-{
- "city": "San Francisco",
- "days": 5
-}
-
-
-
-## access_mcp_resource
-Description: Request to access a resource provided by a connected MCP server. Resources represent data sources that can be used as context, such as files, API responses, or system information.
-Parameters:
-- server_name: (required) The name of the MCP server providing the resource
-- uri: (required) The URI identifying the specific resource to access
-Usage:
-
-server name here
-resource URI here
-
-
-Example: Requesting to access an MCP resource
-
-
-weather-server
-weather://san-francisco/current
-
-
-## ask_followup_question
-Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
-
-Parameters:
-- question: (required) A clear, specific question addressing the information needed
-- follow_up: (required) A list of 2-4 suggested answers, each in its own tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
-
-Usage:
-
-Your question here
-
-First suggestion
-Action with mode switch
-
-
-
-Example:
-
-What is the path to the frontend-config.json file?
-
-./src/frontend-config.json
-./config/frontend-config.json
-./frontend-config.json
-
-
-
-## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
-IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must confirm that you've received successful results from the user for any previous tool uses. If not, then DO NOT use this tool.
-Parameters:
-- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-Usage:
-
-
-Your final result description here
-
-
-
-Example: Requesting to attempt completion with a result
-
-
-I've updated the CSS
-
-
-
-## switch_mode
-Description: Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Code mode to make code changes. The user must approve the mode switch.
-Parameters:
-- mode_slug: (required) The slug of the mode to switch to (e.g., "code", "ask", "architect")
-- reason: (optional) The reason for switching modes
-Usage:
-
-Mode slug here
-Reason for switching here
-
-
-Example: Requesting to switch to code mode
-
-code
-Need to make code changes
-
-
-## new_task
-Description: This will let you create a new task instance in the chosen mode using your provided message.
-
-Parameters:
-- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
-- message: (required) The initial user message or instructions for this new task.
-
-Usage:
-
-your-mode-slug-here
-Your initial instructions here
-
-
-Example:
-
-code
-Implement a new feature for the application
-
-
-
-## update_todo_list
-
-**Description:**
-Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
-
-**Checklist Format:**
-- Use a single-level markdown checklist (no nesting or subtasks).
-- List todos in the intended execution order.
-- Status options:
- - [ ] Task description (pending)
- - [x] Task description (completed)
- - [-] Task description (in progress)
-
-**Status Rules:**
-- [ ] = pending (not started)
-- [x] = completed (fully finished, no unresolved issues)
-- [-] = in_progress (currently being worked on)
-
-**Core Principles:**
-- Before updating, always confirm which todos have been completed since the last update.
-- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
-- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
-- Do not remove any unfinished todos unless explicitly instructed.
-- Always retain all unfinished tasks, updating their status as needed.
-- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
-- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
-- Remove tasks only if they are no longer relevant or if the user requests deletion.
-
-**Usage Example:**
-
-
-[x] Analyze requirements
-[x] Design architecture
-[-] Implement core logic
-[ ] Write tests
-[ ] Update documentation
-
-
-
-*After completing "Implement core logic" and starting "Write tests":*
-
-
-[x] Analyze requirements
-[x] Design architecture
-[x] Implement core logic
-[-] Write tests
-[ ] Update documentation
-[ ] Add performance benchmarks
-
-
-
-**When to Use:**
-- The task is complicated or involves multiple steps or requires ongoing tracking.
-- You need to update the status of several todos at once.
-- New actionable items are discovered during task execution.
-- The user requests a todo list or provides multiple tasks.
-- The task is complex and benefits from clear, stepwise progress tracking.
-
-**When NOT to Use:**
-- There is only a single, trivial task.
-- The task can be completed in one or two simple steps.
-- The request is purely conversational or informational.
-
-**Task Management Guidelines:**
-- Mark task as completed immediately after all work of the current task is done.
-- Start the next task by marking it as in_progress.
-- Add new todos as soon as they are identified.
-- Use clear, descriptive task names.
-
-
-# Tool Use Guidelines
-
-1. Assess what information you already have and what information you need to proceed with the task.
-2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like `ls` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
-3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
-4. Formulate your tool use using the XML format specified for each tool.
-5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
- - Information about whether the tool succeeded or failed, along with any reasons for failure.
- - Linter errors that may have arisen due to the changes you made, which you'll need to address.
- - New terminal output in reaction to the changes, which you may need to consider or act upon.
- - Any other relevant feedback or information related to the tool use.
-6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
-
-It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:
-1. Confirm the success of each step before proceeding.
-2. Address any issues or errors that arise immediately.
-3. Adapt your approach based on new information or unexpected results.
-4. Ensure that each action builds correctly on the previous ones.
-
-By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
-
-MCP SERVERS
-
-The Model Context Protocol (MCP) enables communication between the system and MCP servers that provide additional tools and resources to extend your capabilities. MCP servers can be one of two types:
-
-1. Local (Stdio-based) servers: These run locally on the user's machine and communicate via standard input/output
-2. Remote (SSE-based) servers: These run on remote machines and communicate via Server-Sent Events (SSE) over HTTP/HTTPS
-
-# Connected MCP Servers
-
-When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
-
-
-## Creating an MCP Server
-
-The user may ask you something along the lines of "add a tool" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. If they do, you should obtain detailed instructions on this topic using the fetch_instructions tool, like this:
-
-create_mcp_server
-
-
-====
-
-CAPABILITIES
-
-- You have access to tools that let you execute CLI commands on the user's computer, list files, view source code definitions, regex search, read and write files, and ask follow-up questions. These tools help you effectively accomplish a wide range of tasks, such as writing code, making edits or improvements to existing files, understanding the current state of a project, performing system operations, and much more.
-- When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
-- You can use the execute_command tool to run commands on the user's computer whenever you feel it can help accomplish the user's task. When you need to execute a CLI command, you must provide a clear explanation of what the command does. Prefer to execute complex CLI commands over creating executable scripts, since they are more flexible and easier to run. Interactive and long-running commands are allowed, since the commands are run in the user's VSCode terminal. The user may keep commands running in the background and you will be kept updated on their status along the way. Each command you execute is run in a new terminal instance.
-- You have access to MCP servers that may provide additional tools and resources. Each server may provide different capabilities that you can use to accomplish tasks more effectively.
-
-
-====
-
-MODES
-
-- Test modes section
-
-====
-
-RULES
-
-- The project base directory is: /test/path
-- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to .
-- You cannot `cd` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path.
-- Do not use the ~ character or $HOME to refer to the home directory.
-- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
-- Some modes have restrictions on which files they can edit. If you attempt to edit a restricted file, the operation will be rejected with a FileRestrictionError that will specify which file patterns are allowed for the current mode.
-- Be sure to consider the type of project (e.g. Python, JavaScript, web application) when determining the appropriate structure and files to include. Also consider what files may be most relevant to accomplishing the task, for example looking at a project's manifest file would help you understand the project's dependencies, which you could incorporate into any code you write.
- * For example, in architect mode trying to edit app.js would be rejected because architect mode can only edit files matching "\.md$"
-- When making changes to code, always consider the context in which the code is being used. Ensure that your changes are compatible with the existing codebase and that they follow the project's coding standards and best practices.
-- Do not ask for more information than necessary. Use the tools provided to accomplish the user's request efficiently and effectively. When you've completed your task, you must use the attempt_completion tool to present the result to the user. The user may provide feedback, which you can use to make improvements and try again.
-- You are only allowed to ask the user questions using the ask_followup_question tool. Use this tool only when you need additional details to complete a task, and be sure to use a clear and concise question that will help you move forward with the task. When you ask a question, provide the user with 2-4 suggested answers based on your question so they don't need to do so much typing. The suggestions should be specific, actionable, and directly related to the completed task. They should be ordered by priority or logical sequence. However if you can use the available tools to avoid having to ask the user questions, you should do so. For example, if the user mentions a file that may be in an outside directory like the Desktop, you should use the list_files tool to list the files in the Desktop and check if the file they are talking about is there, rather than asking the user to provide the file path themselves.
-- When executing commands, if you don't see the expected output, assume the terminal executed the command successfully and proceed with the task. The user's terminal may be unable to stream the output back properly. If you absolutely need to see the actual terminal output, use the ask_followup_question tool to request the user to copy and paste it back to you.
-- The user may provide a file's contents directly in their message, in which case you shouldn't use the read_file tool to get the file contents again since you already have it.
-- Your goal is to try to accomplish the user's task, NOT engage in a back and forth conversation.
-- NEVER end attempt_completion result with a question or request to engage in further conversation! Formulate the end of your result in a way that is final and does not require further input from the user.
-- You are STRICTLY FORBIDDEN from starting your messages with "Great", "Certainly", "Okay", "Sure". You should NOT be conversational in your responses, but rather direct and to the point. For example you should NOT say "Great, I've updated the CSS" but instead something like "I've updated the CSS". It is important you be clear and technical in your messages.
-- When presented with images, utilize your vision capabilities to thoroughly examine them and extract meaningful information. Incorporate these insights into your thought process as you accomplish the user's task.
-- At the end of each user message, you will automatically receive environment_details. This information is not written by the user themselves, but is auto-generated to provide potentially relevant context about the project structure and environment. While this information can be valuable for understanding the project context, do not treat it as a direct part of the user's request or response. Use it to inform your actions and decisions, but don't assume the user is explicitly asking about or referring to this information unless they clearly do so in their message. When using environment_details, explain your actions clearly to ensure the user understands, as they may not be aware of these details.
-- Before executing commands, check the "Actively Running Terminals" section in environment_details. If present, consider how these active processes might impact your task. For example, if a local development server is already running, you wouldn't need to start it again. If no active terminals are listed, proceed with command execution as normal.
-- MCP operations should be used one at a time, similar to other tool usage. Wait for confirmation of success before proceeding with additional operations.
-- It is critical you wait for the user's response after each tool use, in order to confirm the success of the tool use. For example, if asked to make a todo app, you would create a file, wait for the user's response it was created successfully, then create another file if needed, wait for the user's response it was created successfully, etc.
-
-====
-
-SYSTEM INFORMATION
-
-Operating System: Linux
-Default Shell: /bin/zsh
-Home Directory: /home/user
-Current Workspace Directory: /test/path
-
-The Current Workspace Directory is the active VS Code project directory, and is therefore the default directory for all tool operations. New terminals will be created in the current workspace directory, however if you change directories in a terminal it will then have a different working directory; changing directories in a terminal does not modify the workspace directory, because you do not have access to change the workspace directory. When the user initially gives you a task, a recursive list of all filepaths in the current workspace directory ('/test/path') will be included in environment_details. This provides an overview of the project's file structure, offering key insights into the project from directory/file names (how developers conceptualize and organize their code) and file extensions (the language used). This can also guide decision-making on which files to explore further. If you need to further explore directories such as outside the current workspace directory, you can use the list_files tool. If you pass 'true' for the recursive parameter, it will list files recursively. Otherwise, it will list files at the top level, which is better suited for generic directories where you don't necessarily need the nested structure, like the Desktop.
-
-====
-
-OBJECTIVE
-
-You accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.
-
-1. Analyze the user's task and set clear, achievable goals to accomplish it. Prioritize these goals in a logical order.
-2. Work through these goals sequentially, utilizing available tools one at a time as necessary. Each goal should correspond to a distinct step in your problem-solving process. You will be informed on the work completed and what's remaining as you go.
-3. Remember, you have extensive capabilities with access to a wide range of tools that can be used in powerful and clever ways as necessary to accomplish each goal. Before calling a tool, do some analysis. First, analyze the file structure provided in environment_details to gain context and insights for proceeding effectively. Next, think about which of the provided tools is the most relevant tool to accomplish the user's task. Go through each of the required parameters of the relevant tool and determine if the user has directly provided or given enough information to infer a value. When deciding if the parameter can be inferred, carefully consider all the context to see if it supports a specific value. If all of the required parameters are present or can be reasonably inferred, proceed with the tool use. BUT, if one of the values for a required parameter is missing, DO NOT invoke the tool (not even with fillers for the missing params) and instead, ask the user to provide the missing parameters using the ask_followup_question tool. DO NOT ask for more information on optional parameters if it is not provided.
-4. Once you've completed the user's task, you must use the attempt_completion tool to present the result of the task to the user.
-5. The user may provide feedback, which you can use to make improvements and try again. But DO NOT continue in pointless back and forth conversations, i.e. don't end your responses with questions or offers for further assistance.
-
-
-====
-
-USER'S CUSTOM INSTRUCTIONS
-
-The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
-
-Language Preference:
-You should always speak and think in the "en" language.
-
-Mode-specific Instructions:
-1. Do some information gathering (using provided tools) to get more context about the task.
-
-2. You should also ask the user clarifying questions to get a better understanding of the task.
-
-3. Once you've gained more context about the user's request, break down the task into clear, actionable steps and create a todo list using the `update_todo_list` tool. Each todo item should be:
- - Specific and actionable
- - Listed in logical execution order
- - Focused on a single, well-defined outcome
- - Clear enough that another mode could execute it independently
-
- **Note:** If the `update_todo_list` tool is not available, write the plan to a markdown file (e.g., `plan.md` or `todo.md`) instead.
-
-4. As you gather more information or discover new requirements, update the todo list to reflect the current understanding of what needs to be accomplished.
-
-5. Ask the user if they are pleased with this plan, or if they would like to make any changes. Think of this as a brainstorming session where you can discuss the task and refine the todo list.
-
-6. Include Mermaid diagrams if they help clarify complex workflows or system architecture. Please avoid using double quotes ("") and parentheses () inside square brackets ([]) in Mermaid diagrams, as this can cause parsing errors.
-
-7. Use the switch_mode tool to request that the user switch to another mode to implement the solution.
-
-**IMPORTANT: Focus on creating clear, actionable todo lists rather than lengthy markdown documents. Use the todo list as your primary planning tool to track and organize the work that needs to be done.**
-
-**CRITICAL: Never provide level of effort time estimates (e.g., hours, days, weeks) for tasks. Focus solely on breaking down the work into clear, actionable steps without estimating how long they will take.**
-
-Unless told otherwise, if you want to save a plan file, put it in the /plans directory
-
-Rules:
-# Rules from .clinerules-architect:
-Mock mode-specific rules
-# Rules from .clinerules:
-Mock generic rules
\ No newline at end of file
diff --git a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap
index ac93623fda..5bed6df09d 100644
--- a/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap
+++ b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap
@@ -10,366 +10,15 @@ ALL responses MUST show ANY `language construct` OR filename reference as clicka
TOOL USE
-You have access to a set of tools that are executed upon the user's approval. You must use exactly one tool per message, and every assistant message must include a tool call. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
+You have access to a set of tools that are executed upon the user's approval. Use the provider-native tool-calling mechanism. Do not include XML markup or examples. You must call at least one tool per assistant response. Prefer calling as many tools as are reasonably needed in a single response to reduce back-and-forth and complete tasks faster.
-# Tool Use Formatting
-
-Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:
-
-
-value1
-value2
-...
-
-
-Always use the actual tool name as the XML tag name for proper parsing and execution.
-
-# Tools
-
-## read_file
-Description: Request to read the contents of one or more files. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when creating diffs or discussing code. Use line ranges to efficiently read specific portions of large files. Supports text extraction from PDF and DOCX files, but may not handle other binary files properly.
-
-**IMPORTANT: You can read a maximum of 5 files in a single request.** If you need to read more files, use multiple sequential read_file requests.
-
-By specifying line ranges, you can efficiently read specific portions of large files without loading the entire file into memory.
-Parameters:
-- args: Contains one or more file elements, where each file contains:
- - path: (required) File path (relative to workspace directory /test/path)
- - line_range: (optional) One or more line range elements in format "start-end" (1-based, inclusive)
-
-Usage:
-
-
-
- path/to/file
- start-end
-
-
-
-
-Examples:
-
-1. Reading a single file:
-
-
-
- src/app.ts
- 1-1000
-
-
-
-
-2. Reading multiple files (within the 5-file limit):
-
-
-
- src/app.ts
- 1-50
- 100-150
-
-
- src/utils.ts
- 10-20
-
-
-
-
-3. Reading an entire file:
-
-
-
- config.json
-
-
-
-
-IMPORTANT: You MUST use this Efficient Reading Strategy:
-- You MUST read all related files and implementations together in a single operation (up to 5 files at once)
-- You MUST obtain all necessary context before proceeding with changes
-- You MUST use line ranges to read specific portions of large files, rather than reading entire files when not needed
-- You MUST combine adjacent line ranges (<10 lines apart)
-- You MUST use multiple ranges for content separated by >10 lines
-- You MUST include sufficient line context for planned modifications while keeping ranges minimal
-
-- When you need to read more than 5 files, prioritize the most critical files first, then use subsequent read_file requests for additional files
-
-## fetch_instructions
-Description: Request to fetch instructions to perform a task
-Parameters:
-- task: (required) The task to get instructions for. This can take the following values:
- create_mcp_server
- create_mode
-
-Example: Requesting instructions to create an MCP Server
-
-
-create_mcp_server
-
-
-## search_files
-Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
-
-Craft your regex patterns carefully to balance specificity and flexibility. Use this tool to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include surrounding context, so analyze the surrounding code to better understand the matches. Leverage this tool in combination with other tools for more comprehensive analysis - for example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches.
-
-Parameters:
-- path: (required) The path of the directory to search in (relative to the current workspace directory /test/path). This directory will be recursively searched.
-- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax.
-- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*).
-
-Usage:
-
-Directory path here
-Your regex pattern here
-file pattern here (optional)
-
-
-Example: Searching for all .ts files in the current directory
-
-.
-.*
-*.ts
-
-
-Example: Searching for function definitions in JavaScript files
-
-src
-function\s+\w+
-*.js
-
-
-## list_files
-Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.
-Parameters:
-- path: (required) The path of the directory to list contents for (relative to the current workspace directory /test/path)
-- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
-Usage:
-
-Directory path here
-true or false (optional)
-
-
-Example: Requesting to list all files in the current directory
-
-.
-false
-
-
-## write_to_file
-Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
-
-**Important:** You should prefer using other editing tools over write_to_file when making changes to existing files, since write_to_file is slower and cannot handle large files. Use write_to_file primarily for new file creation.
-
-When using this tool, use it directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code.
-
-When creating a new project, organize all new files within a dedicated project directory unless the user specifies otherwise. Structure the project logically, adhering to best practices for the specific type of project being created.
-
-Parameters:
-- path: (required) The path of the file to write to (relative to the current workspace directory /test/path)
-- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content.
-
-Usage:
-
-File path here
-
-Your file content here
-
-
-
-Example: Writing a configuration file
-
-frontend-config.json
-
-{
- "apiEndpoint": "https://api.example.com",
- "theme": {
- "primaryColor": "#007bff",
- "secondaryColor": "#6c757d",
- "fontFamily": "Arial, sans-serif"
- },
- "features": {
- "darkMode": true,
- "notifications": true,
- "analytics": false
- },
- "version": "1.0.0"
-}
-
-
-
-## ask_followup_question
-Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
-
-Parameters:
-- question: (required) A clear, specific question addressing the information needed
-- follow_up: (required) A list of 2-4 suggested answers, each in its own tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
-
-Usage:
-
-Your question here
-
-First suggestion
-Action with mode switch
-
-
-
-Example:
-
-What is the path to the frontend-config.json file?
-
-./src/frontend-config.json
-./config/frontend-config.json
-./frontend-config.json
-
-
-
-## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
-IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must confirm that you've received successful results from the user for any previous tool uses. If not, then DO NOT use this tool.
-Parameters:
-- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-Usage:
-
-
-Your final result description here
-
-
-
-Example: Requesting to attempt completion with a result
-
-
-I've updated the CSS
-
-
-
-## switch_mode
-Description: Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Code mode to make code changes. The user must approve the mode switch.
-Parameters:
-- mode_slug: (required) The slug of the mode to switch to (e.g., "code", "ask", "architect")
-- reason: (optional) The reason for switching modes
-Usage:
-
-Mode slug here
-Reason for switching here
-
-
-Example: Requesting to switch to code mode
-
-code
-Need to make code changes
-
-
-## new_task
-Description: This will let you create a new task instance in the chosen mode using your provided message.
-
-Parameters:
-- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
-- message: (required) The initial user message or instructions for this new task.
-
-Usage:
-
-your-mode-slug-here
-Your initial instructions here
-
-
-Example:
-
-code
-Implement a new feature for the application
-
-
-
-## update_todo_list
-
-**Description:**
-Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
-
-**Checklist Format:**
-- Use a single-level markdown checklist (no nesting or subtasks).
-- List todos in the intended execution order.
-- Status options:
- - [ ] Task description (pending)
- - [x] Task description (completed)
- - [-] Task description (in progress)
-
-**Status Rules:**
-- [ ] = pending (not started)
-- [x] = completed (fully finished, no unresolved issues)
-- [-] = in_progress (currently being worked on)
-
-**Core Principles:**
-- Before updating, always confirm which todos have been completed since the last update.
-- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
-- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
-- Do not remove any unfinished todos unless explicitly instructed.
-- Always retain all unfinished tasks, updating their status as needed.
-- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
-- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
-- Remove tasks only if they are no longer relevant or if the user requests deletion.
-
-**Usage Example:**
-
-
-[x] Analyze requirements
-[x] Design architecture
-[-] Implement core logic
-[ ] Write tests
-[ ] Update documentation
-
-
-
-*After completing "Implement core logic" and starting "Write tests":*
-
-
-[x] Analyze requirements
-[x] Design architecture
-[x] Implement core logic
-[-] Write tests
-[ ] Update documentation
-[ ] Add performance benchmarks
-
-
-
-**When to Use:**
-- The task is complicated or involves multiple steps or requires ongoing tracking.
-- You need to update the status of several todos at once.
-- New actionable items are discovered during task execution.
-- The user requests a todo list or provides multiple tasks.
-- The task is complex and benefits from clear, stepwise progress tracking.
-
-**When NOT to Use:**
-- There is only a single, trivial task.
-- The task can be completed in one or two simple steps.
-- The request is purely conversational or informational.
-
-**Task Management Guidelines:**
-- Mark task as completed immediately after all work of the current task is done.
-- Start the next task by marking it as in_progress.
-- Add new todos as soon as they are identified.
-- Use clear, descriptive task names.
-
-
-# Tool Use Guidelines
+ # Tool Use Guidelines
1. Assess what information you already have and what information you need to proceed with the task.
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like `ls` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
-3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
-4. Formulate your tool use using the XML format specified for each tool.
-5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
- - Information about whether the tool succeeded or failed, along with any reasons for failure.
- - Linter errors that may have arisen due to the changes you made, which you'll need to address.
- - New terminal output in reaction to the changes, which you may need to consider or act upon.
- - Any other relevant feedback or information related to the tool use.
-6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
-
-It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:
-1. Confirm the success of each step before proceeding.
-2. Address any issues or errors that arise immediately.
-3. Adapt your approach based on new information or unexpected results.
-4. Ensure that each action builds correctly on the previous ones.
-
-By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
-
+3. If multiple actions are needed, you may use multiple tools in a single message when appropriate, or use tools iteratively across messages. Each tool use should be informed by the results of previous tool uses. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
+By carefully considering the user's response after tool executions, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
====
@@ -390,7 +39,7 @@ MODES
RULES
- The project base directory is: /test/path
-- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to .
+- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to execute_command.
- You cannot `cd` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path.
- Do not use the ~ character or $HOME to refer to the home directory.
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap
index ee8a50e993..42e8bba9c6 100644
--- a/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap
+++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/consistent-system-prompt.snap
@@ -10,361 +10,15 @@ ALL responses MUST show ANY `language construct` OR filename reference as clicka
TOOL USE
-You have access to a set of tools that are executed upon the user's approval. You must use exactly one tool per message, and every assistant message must include a tool call. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
+You have access to a set of tools that are executed upon the user's approval. Use the provider-native tool-calling mechanism. Do not include XML markup or examples. You must call at least one tool per assistant response. Prefer calling as many tools as are reasonably needed in a single response to reduce back-and-forth and complete tasks faster.
-# Tool Use Formatting
-
-Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:
-
-
-value1
-value2
-...
-
-
-Always use the actual tool name as the XML tag name for proper parsing and execution.
-
-# Tools
-
-## read_file
-Description: Request to read the contents of one or more files. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when creating diffs or discussing code. Supports text extraction from PDF and DOCX files, but may not handle other binary files properly.
-
-**IMPORTANT: You can read a maximum of 5 files in a single request.** If you need to read more files, use multiple sequential read_file requests.
-
-
-Parameters:
-- args: Contains one or more file elements, where each file contains:
- - path: (required) File path (relative to workspace directory /test/path)
-
-
-Usage:
-
-
-
- path/to/file
-
-
-
-
-
-Examples:
-
-1. Reading a single file:
-
-
-
- src/app.ts
-
-
-
-
-
-2. Reading multiple files (within the 5-file limit):
-
-
-
- src/app.ts
-
-
-
- src/utils.ts
-
-
-
-
-
-3. Reading an entire file:
-
-
-
- config.json
-
-
-
-
-IMPORTANT: You MUST use this Efficient Reading Strategy:
-- You MUST read all related files and implementations together in a single operation (up to 5 files at once)
-- You MUST obtain all necessary context before proceeding with changes
-
-- When you need to read more than 5 files, prioritize the most critical files first, then use subsequent read_file requests for additional files
-
-## fetch_instructions
-Description: Request to fetch instructions to perform a task
-Parameters:
-- task: (required) The task to get instructions for. This can take the following values:
- create_mcp_server
- create_mode
-
-Example: Requesting instructions to create an MCP Server
-
-
-create_mcp_server
-
-
-## search_files
-Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
-
-Craft your regex patterns carefully to balance specificity and flexibility. Use this tool to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include surrounding context, so analyze the surrounding code to better understand the matches. Leverage this tool in combination with other tools for more comprehensive analysis - for example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches.
-
-Parameters:
-- path: (required) The path of the directory to search in (relative to the current workspace directory /test/path). This directory will be recursively searched.
-- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax.
-- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*).
-
-Usage:
-
-Directory path here
-Your regex pattern here
-file pattern here (optional)
-
-
-Example: Searching for all .ts files in the current directory
-
-.
-.*
-*.ts
-
-
-Example: Searching for function definitions in JavaScript files
-
-src
-function\s+\w+
-*.js
-
-
-## list_files
-Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.
-Parameters:
-- path: (required) The path of the directory to list contents for (relative to the current workspace directory /test/path)
-- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
-Usage:
-
-Directory path here
-true or false (optional)
-
-
-Example: Requesting to list all files in the current directory
-
-.
-false
-
-
-## write_to_file
-Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
-
-**Important:** You should prefer using other editing tools over write_to_file when making changes to existing files, since write_to_file is slower and cannot handle large files. Use write_to_file primarily for new file creation.
-
-When using this tool, use it directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code.
-
-When creating a new project, organize all new files within a dedicated project directory unless the user specifies otherwise. Structure the project logically, adhering to best practices for the specific type of project being created.
-
-Parameters:
-- path: (required) The path of the file to write to (relative to the current workspace directory /test/path)
-- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content.
-
-Usage:
-
-File path here
-
-Your file content here
-
-
-
-Example: Writing a configuration file
-
-frontend-config.json
-
-{
- "apiEndpoint": "https://api.example.com",
- "theme": {
- "primaryColor": "#007bff",
- "secondaryColor": "#6c757d",
- "fontFamily": "Arial, sans-serif"
- },
- "features": {
- "darkMode": true,
- "notifications": true,
- "analytics": false
- },
- "version": "1.0.0"
-}
-
-
-
-## ask_followup_question
-Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
-
-Parameters:
-- question: (required) A clear, specific question addressing the information needed
-- follow_up: (required) A list of 2-4 suggested answers, each in its own tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
-
-Usage:
-
-Your question here
-
-First suggestion
-Action with mode switch
-
-
-
-Example:
-
-What is the path to the frontend-config.json file?
-
-./src/frontend-config.json
-./config/frontend-config.json
-./frontend-config.json
-
-
-
-## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
-IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must confirm that you've received successful results from the user for any previous tool uses. If not, then DO NOT use this tool.
-Parameters:
-- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-Usage:
-
-
-Your final result description here
-
-
-
-Example: Requesting to attempt completion with a result
-
-
-I've updated the CSS
-
-
-
-## switch_mode
-Description: Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Code mode to make code changes. The user must approve the mode switch.
-Parameters:
-- mode_slug: (required) The slug of the mode to switch to (e.g., "code", "ask", "architect")
-- reason: (optional) The reason for switching modes
-Usage:
-
-Mode slug here
-Reason for switching here
-
-
-Example: Requesting to switch to code mode
-
-code
-Need to make code changes
-
-
-## new_task
-Description: This will let you create a new task instance in the chosen mode using your provided message.
-
-Parameters:
-- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
-- message: (required) The initial user message or instructions for this new task.
-
-Usage:
-
-your-mode-slug-here
-Your initial instructions here
-
-
-Example:
-
-code
-Implement a new feature for the application
-
-
-
-## update_todo_list
-
-**Description:**
-Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
-
-**Checklist Format:**
-- Use a single-level markdown checklist (no nesting or subtasks).
-- List todos in the intended execution order.
-- Status options:
- - [ ] Task description (pending)
- - [x] Task description (completed)
- - [-] Task description (in progress)
-
-**Status Rules:**
-- [ ] = pending (not started)
-- [x] = completed (fully finished, no unresolved issues)
-- [-] = in_progress (currently being worked on)
-
-**Core Principles:**
-- Before updating, always confirm which todos have been completed since the last update.
-- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
-- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
-- Do not remove any unfinished todos unless explicitly instructed.
-- Always retain all unfinished tasks, updating their status as needed.
-- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
-- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
-- Remove tasks only if they are no longer relevant or if the user requests deletion.
-
-**Usage Example:**
-
-
-[x] Analyze requirements
-[x] Design architecture
-[-] Implement core logic
-[ ] Write tests
-[ ] Update documentation
-
-
-
-*After completing "Implement core logic" and starting "Write tests":*
-
-
-[x] Analyze requirements
-[x] Design architecture
-[x] Implement core logic
-[-] Write tests
-[ ] Update documentation
-[ ] Add performance benchmarks
-
-
-
-**When to Use:**
-- The task is complicated or involves multiple steps or requires ongoing tracking.
-- You need to update the status of several todos at once.
-- New actionable items are discovered during task execution.
-- The user requests a todo list or provides multiple tasks.
-- The task is complex and benefits from clear, stepwise progress tracking.
-
-**When NOT to Use:**
-- There is only a single, trivial task.
-- The task can be completed in one or two simple steps.
-- The request is purely conversational or informational.
-
-**Task Management Guidelines:**
-- Mark task as completed immediately after all work of the current task is done.
-- Start the next task by marking it as in_progress.
-- Add new todos as soon as they are identified.
-- Use clear, descriptive task names.
-
-
-# Tool Use Guidelines
+ # Tool Use Guidelines
1. Assess what information you already have and what information you need to proceed with the task.
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like `ls` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
-3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
-4. Formulate your tool use using the XML format specified for each tool.
-5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
- - Information about whether the tool succeeded or failed, along with any reasons for failure.
- - Linter errors that may have arisen due to the changes you made, which you'll need to address.
- - New terminal output in reaction to the changes, which you may need to consider or act upon.
- - Any other relevant feedback or information related to the tool use.
-6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
-
-It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:
-1. Confirm the success of each step before proceeding.
-2. Address any issues or errors that arise immediately.
-3. Adapt your approach based on new information or unexpected results.
-4. Ensure that each action builds correctly on the previous ones.
-
-By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
-
+3. If multiple actions are needed, you may use multiple tools in a single message when appropriate, or use tools iteratively across messages. Each tool use should be informed by the results of previous tool uses. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
+By carefully considering the user's response after tool executions, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
====
@@ -385,7 +39,7 @@ MODES
RULES
- The project base directory is: /test/path
-- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to .
+- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to execute_command.
- You cannot `cd` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path.
- Do not use the ~ character or $HOME to refer to the home directory.
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
@@ -434,7 +88,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
USER'S CUSTOM INSTRUCTIONS
-The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
+The following additional instructions are provided by the user, and should be followed to the best of your ability.
Language Preference:
You should always speak and think in the "en" language.
diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap
index 8edc23260e..42e8bba9c6 100644
--- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap
+++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-computer-use-support.snap
@@ -10,446 +10,15 @@ ALL responses MUST show ANY `language construct` OR filename reference as clicka
TOOL USE
-You have access to a set of tools that are executed upon the user's approval. You must use exactly one tool per message, and every assistant message must include a tool call. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
+You have access to a set of tools that are executed upon the user's approval. Use the provider-native tool-calling mechanism. Do not include XML markup or examples. You must call at least one tool per assistant response. Prefer calling as many tools as are reasonably needed in a single response to reduce back-and-forth and complete tasks faster.
-# Tool Use Formatting
-
-Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:
-
-
-value1
-value2
-...
-
-
-Always use the actual tool name as the XML tag name for proper parsing and execution.
-
-# Tools
-
-## read_file
-Description: Request to read the contents of one or more files. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when creating diffs or discussing code. Supports text extraction from PDF and DOCX files, but may not handle other binary files properly.
-
-**IMPORTANT: You can read a maximum of 5 files in a single request.** If you need to read more files, use multiple sequential read_file requests.
-
-
-Parameters:
-- args: Contains one or more file elements, where each file contains:
- - path: (required) File path (relative to workspace directory /test/path)
-
-
-Usage:
-
-
-
- path/to/file
-
-
-
-
-
-Examples:
-
-1. Reading a single file:
-
-
-
- src/app.ts
-
-
-
-
-
-2. Reading multiple files (within the 5-file limit):
-
-
-
- src/app.ts
-
-
-
- src/utils.ts
-
-
-
-
-
-3. Reading an entire file:
-
-
-
- config.json
-
-
-
-
-IMPORTANT: You MUST use this Efficient Reading Strategy:
-- You MUST read all related files and implementations together in a single operation (up to 5 files at once)
-- You MUST obtain all necessary context before proceeding with changes
-
-- When you need to read more than 5 files, prioritize the most critical files first, then use subsequent read_file requests for additional files
-
-## fetch_instructions
-Description: Request to fetch instructions to perform a task
-Parameters:
-- task: (required) The task to get instructions for. This can take the following values:
- create_mcp_server
- create_mode
-
-Example: Requesting instructions to create an MCP Server
-
-
-create_mcp_server
-
-
-## search_files
-Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
-
-Craft your regex patterns carefully to balance specificity and flexibility. Use this tool to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include surrounding context, so analyze the surrounding code to better understand the matches. Leverage this tool in combination with other tools for more comprehensive analysis - for example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches.
-
-Parameters:
-- path: (required) The path of the directory to search in (relative to the current workspace directory /test/path). This directory will be recursively searched.
-- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax.
-- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*).
-
-Usage:
-
-Directory path here
-Your regex pattern here
-file pattern here (optional)
-
-
-Example: Searching for all .ts files in the current directory
-
-.
-.*
-*.ts
-
-
-Example: Searching for function definitions in JavaScript files
-
-src
-function\s+\w+
-*.js
-
-
-## list_files
-Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.
-Parameters:
-- path: (required) The path of the directory to list contents for (relative to the current workspace directory /test/path)
-- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
-Usage:
-
-Directory path here
-true or false (optional)
-
-
-Example: Requesting to list all files in the current directory
-
-.
-false
-
-
-## write_to_file
-Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
-
-**Important:** You should prefer using other editing tools over write_to_file when making changes to existing files, since write_to_file is slower and cannot handle large files. Use write_to_file primarily for new file creation.
-
-When using this tool, use it directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code.
-
-When creating a new project, organize all new files within a dedicated project directory unless the user specifies otherwise. Structure the project logically, adhering to best practices for the specific type of project being created.
-
-Parameters:
-- path: (required) The path of the file to write to (relative to the current workspace directory /test/path)
-- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content.
-
-Usage:
-
-File path here
-
-Your file content here
-
-
-
-Example: Writing a configuration file
-
-frontend-config.json
-
-{
- "apiEndpoint": "https://api.example.com",
- "theme": {
- "primaryColor": "#007bff",
- "secondaryColor": "#6c757d",
- "fontFamily": "Arial, sans-serif"
- },
- "features": {
- "darkMode": true,
- "notifications": true,
- "analytics": false
- },
- "version": "1.0.0"
-}
-
-
-
-## browser_action
-Description: Request to interact with a Puppeteer-controlled browser. Every action, except `close`, will be responded to with a screenshot of the browser's current state, along with any new console logs. You may only perform one browser action per message, and wait for the user's response including a screenshot and logs to determine the next action.
-
-This tool is particularly useful for web development tasks as it allows you to launch a browser, navigate to pages, interact with elements through clicks and keyboard input, and capture the results through screenshots and console logs. Use it at key stages of web development tasks - such as after implementing new features, making substantial changes, when troubleshooting issues, or to verify the result of your work. Analyze the provided screenshots to ensure correct rendering or identify errors, and review console logs for runtime issues.
-
-The user may ask generic non-development tasks (such as "what's the latest news" or "look up the weather"), in which case you might use this tool to complete the task if it makes sense to do so, rather than trying to create a website or using curl to answer the question. However, if an available MCP server tool or resource can be used instead, you should prefer to use it over browser_action.
-
-**Browser Session Lifecycle:**
-- Browser sessions **start** with `launch` and **end** with `close`
-- The session remains active across multiple messages and tool uses
-- You can use other tools while the browser session is active - it will stay open in the background
-
-Parameters:
-- action: (required) The action to perform. The available actions are:
- * launch: Launch a new Puppeteer-controlled browser instance at the specified URL. This **must always be the first action**.
- - Use with the `url` parameter to provide the URL.
- - Ensure the URL is valid and includes the appropriate protocol (e.g. http://localhost:3000/page, file:///path/to/file.html, etc.)
- * hover: Move the cursor to a specific x,y coordinate.
- - Use with the `coordinate` parameter to specify the location.
- - Always move to the center of an element (icon, button, link, etc.) based on coordinates derived from a screenshot.
- * click: Click at a specific x,y coordinate.
- - Use with the `coordinate` parameter to specify the location.
- - Always click in the center of an element (icon, button, link, etc.) based on coordinates derived from a screenshot.
- * type: Type a string of text on the keyboard. You might use this after clicking on a text field to input text.
- - Use with the `text` parameter to provide the string to type.
- * press: Press a single keyboard key or key combination (e.g., Enter, Tab, Escape, Cmd+K, Shift+Enter).
- - Use with the `text` parameter to provide the key name or combination.
- - For single keys: Enter, Tab, Escape, etc.
- - For key combinations: Cmd+K, Ctrl+C, Shift+Enter, Alt+F4, etc.
- - Supported modifiers: Cmd/Command/Meta, Ctrl/Control, Shift, Alt/Option
- - Example: Cmd+K or Shift+Enter
- * resize: Resize the viewport to a specific w,h size.
- - Use with the `size` parameter to specify the new size.
- * scroll_down: Scroll down the page by one page height.
- * scroll_up: Scroll up the page by one page height.
- * screenshot: Take a screenshot and save it to a file.
- - Use with the `path` parameter to specify the destination file path.
- - Supported formats: .png, .jpeg, .webp
- - Example: `screenshot` with `screenshots/result.png`
- * close: Close the Puppeteer-controlled browser instance. This **must always be the final browser action**.
- - Example: `close`
-- url: (optional) Use this for providing the URL for the `launch` action.
- * Example: https://example.com
-- coordinate: (optional) The X and Y coordinates for the `click` and `hover` actions.
- * **CRITICAL**: Screenshot dimensions are NOT the same as the browser viewport dimensions
- * Format: x,y@widthxheight
- * Measure x,y on the screenshot image you see in chat
- * The widthxheight MUST be the EXACT pixel size of that screenshot image (never the browser viewport)
- * Never use the browser viewport size for widthxheight - the viewport is only a reference and is often larger than the screenshot
- * Images are often downscaled before you see them, so the screenshot's dimensions will likely be smaller than the viewport
- * Example A: If the screenshot you see is 1094x1092 and you want to click (450,300) on that image, use: 450,300@1094x1092
- * Example B: If the browser viewport is 1280x800 but the screenshot is 1000x625 and you want to click (500,300) on the screenshot, use: 500,300@1000x625
-- size: (optional) The width and height for the `resize` action.
- * Example: 1280,720
-- text: (optional) Use this for providing the text for the `type` action.
- * Example: Hello, world!
-- path: (optional) File path for the `screenshot` action. Path is relative to the workspace.
- * Supported formats: .png, .jpeg, .webp
- * Example: screenshots/my-screenshot.png
-Usage:
-
-Action to perform (e.g., launch, click, type, press, scroll_down, scroll_up, close)
-URL to launch the browser at (optional)
-x,y@widthxheight coordinates (optional)
-Text to type (optional)
-
-
-Example: Requesting to launch a browser at https://example.com
-
-launch
-https://example.com
-
-
-Example: Requesting to click on the element at coordinates 450,300 on a 1024x768 image
-
-click
-450,300@1024x768
-
-
-Example: Taking a screenshot and saving it to a file
-
-screenshot
-screenshots/result.png
-
-
-## ask_followup_question
-Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
-
-Parameters:
-- question: (required) A clear, specific question addressing the information needed
-- follow_up: (required) A list of 2-4 suggested answers, each in its own tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
-
-Usage:
-
-Your question here
-
-First suggestion
-Action with mode switch
-
-
-
-Example:
-
-What is the path to the frontend-config.json file?
-
-./src/frontend-config.json
-./config/frontend-config.json
-./frontend-config.json
-
-
-
-## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
-IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must confirm that you've received successful results from the user for any previous tool uses. If not, then DO NOT use this tool.
-Parameters:
-- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-Usage:
-
-
-Your final result description here
-
-
-
-Example: Requesting to attempt completion with a result
-
-
-I've updated the CSS
-
-
-
-## switch_mode
-Description: Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Code mode to make code changes. The user must approve the mode switch.
-Parameters:
-- mode_slug: (required) The slug of the mode to switch to (e.g., "code", "ask", "architect")
-- reason: (optional) The reason for switching modes
-Usage:
-
-Mode slug here
-Reason for switching here
-
-
-Example: Requesting to switch to code mode
-
-code
-Need to make code changes
-
-
-## new_task
-Description: This will let you create a new task instance in the chosen mode using your provided message.
-
-Parameters:
-- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
-- message: (required) The initial user message or instructions for this new task.
-
-Usage:
-
-your-mode-slug-here
-Your initial instructions here
-
-
-Example:
-
-code
-Implement a new feature for the application
-
-
-
-## update_todo_list
-
-**Description:**
-Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
-
-**Checklist Format:**
-- Use a single-level markdown checklist (no nesting or subtasks).
-- List todos in the intended execution order.
-- Status options:
- - [ ] Task description (pending)
- - [x] Task description (completed)
- - [-] Task description (in progress)
-
-**Status Rules:**
-- [ ] = pending (not started)
-- [x] = completed (fully finished, no unresolved issues)
-- [-] = in_progress (currently being worked on)
-
-**Core Principles:**
-- Before updating, always confirm which todos have been completed since the last update.
-- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
-- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
-- Do not remove any unfinished todos unless explicitly instructed.
-- Always retain all unfinished tasks, updating their status as needed.
-- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
-- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
-- Remove tasks only if they are no longer relevant or if the user requests deletion.
-
-**Usage Example:**
-
-
-[x] Analyze requirements
-[x] Design architecture
-[-] Implement core logic
-[ ] Write tests
-[ ] Update documentation
-
-
-
-*After completing "Implement core logic" and starting "Write tests":*
-
-
-[x] Analyze requirements
-[x] Design architecture
-[x] Implement core logic
-[-] Write tests
-[ ] Update documentation
-[ ] Add performance benchmarks
-
-
-
-**When to Use:**
-- The task is complicated or involves multiple steps or requires ongoing tracking.
-- You need to update the status of several todos at once.
-- New actionable items are discovered during task execution.
-- The user requests a todo list or provides multiple tasks.
-- The task is complex and benefits from clear, stepwise progress tracking.
-
-**When NOT to Use:**
-- There is only a single, trivial task.
-- The task can be completed in one or two simple steps.
-- The request is purely conversational or informational.
-
-**Task Management Guidelines:**
-- Mark task as completed immediately after all work of the current task is done.
-- Start the next task by marking it as in_progress.
-- Add new todos as soon as they are identified.
-- Use clear, descriptive task names.
-
-
-# Tool Use Guidelines
+ # Tool Use Guidelines
1. Assess what information you already have and what information you need to proceed with the task.
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like `ls` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
-3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
-4. Formulate your tool use using the XML format specified for each tool.
-5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
- - Information about whether the tool succeeded or failed, along with any reasons for failure.
- - Linter errors that may have arisen due to the changes you made, which you'll need to address.
- - New terminal output in reaction to the changes, which you may need to consider or act upon.
- - Any other relevant feedback or information related to the tool use.
-6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
-
-It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:
-1. Confirm the success of each step before proceeding.
-2. Address any issues or errors that arise immediately.
-3. Adapt your approach based on new information or unexpected results.
-4. Ensure that each action builds correctly on the previous ones.
-
-By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
-
+3. If multiple actions are needed, you may use multiple tools in a single message when appropriate, or use tools iteratively across messages. Each tool use should be informed by the results of previous tool uses. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
+By carefully considering the user's response after tool executions, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
====
@@ -470,7 +39,7 @@ MODES
RULES
- The project base directory is: /test/path
-- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to .
+- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to execute_command.
- You cannot `cd` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path.
- Do not use the ~ character or $HOME to refer to the home directory.
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
@@ -519,7 +88,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
USER'S CUSTOM INSTRUCTIONS
-The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
+The following additional instructions are provided by the user, and should be followed to the best of your ability.
Language Preference:
You should always speak and think in the "en" language.
diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap
index ee8a50e993..d32dc3dc23 100644
--- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap
+++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-false.snap
@@ -10,351 +10,19 @@ ALL responses MUST show ANY `language construct` OR filename reference as clicka
TOOL USE
-You have access to a set of tools that are executed upon the user's approval. You must use exactly one tool per message, and every assistant message must include a tool call. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
+You have access to a set of tools that are executed upon the user's approval. Use the provider-native tool-calling mechanism. Do not include XML markup or examples. You must use exactly one tool call per assistant response. Do not call zero tools or more than one tool in the same response.
-# Tool Use Formatting
-
-Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:
-
-
-value1
-value2
-...
-
-
-Always use the actual tool name as the XML tag name for proper parsing and execution.
-
-# Tools
-
-## read_file
-Description: Request to read the contents of one or more files. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when creating diffs or discussing code. Supports text extraction from PDF and DOCX files, but may not handle other binary files properly.
-
-**IMPORTANT: You can read a maximum of 5 files in a single request.** If you need to read more files, use multiple sequential read_file requests.
-
-
-Parameters:
-- args: Contains one or more file elements, where each file contains:
- - path: (required) File path (relative to workspace directory /test/path)
-
-
-Usage:
-
-
-
- path/to/file
-
-
-
-
-
-Examples:
-
-1. Reading a single file:
-
-
-
- src/app.ts
-
-
-
-
-
-2. Reading multiple files (within the 5-file limit):
-
-
-
- src/app.ts
-
-
-
- src/utils.ts
-
-
-
-
-
-3. Reading an entire file:
-
-
-
- config.json
-
-
-
-
-IMPORTANT: You MUST use this Efficient Reading Strategy:
-- You MUST read all related files and implementations together in a single operation (up to 5 files at once)
-- You MUST obtain all necessary context before proceeding with changes
-
-- When you need to read more than 5 files, prioritize the most critical files first, then use subsequent read_file requests for additional files
-
-## fetch_instructions
-Description: Request to fetch instructions to perform a task
-Parameters:
-- task: (required) The task to get instructions for. This can take the following values:
- create_mcp_server
- create_mode
-
-Example: Requesting instructions to create an MCP Server
-
-
-create_mcp_server
-
-
-## search_files
-Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
-
-Craft your regex patterns carefully to balance specificity and flexibility. Use this tool to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include surrounding context, so analyze the surrounding code to better understand the matches. Leverage this tool in combination with other tools for more comprehensive analysis - for example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches.
-
-Parameters:
-- path: (required) The path of the directory to search in (relative to the current workspace directory /test/path). This directory will be recursively searched.
-- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax.
-- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*).
-
-Usage:
-
-Directory path here
-Your regex pattern here
-file pattern here (optional)
-
-
-Example: Searching for all .ts files in the current directory
-
-.
-.*
-*.ts
-
-
-Example: Searching for function definitions in JavaScript files
-
-src
-function\s+\w+
-*.js
-
-
-## list_files
-Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.
-Parameters:
-- path: (required) The path of the directory to list contents for (relative to the current workspace directory /test/path)
-- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
-Usage:
-
-Directory path here
-true or false (optional)
-
-
-Example: Requesting to list all files in the current directory
-
-.
-false
-
-
-## write_to_file
-Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
-
-**Important:** You should prefer using other editing tools over write_to_file when making changes to existing files, since write_to_file is slower and cannot handle large files. Use write_to_file primarily for new file creation.
-
-When using this tool, use it directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code.
-
-When creating a new project, organize all new files within a dedicated project directory unless the user specifies otherwise. Structure the project logically, adhering to best practices for the specific type of project being created.
-
-Parameters:
-- path: (required) The path of the file to write to (relative to the current workspace directory /test/path)
-- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content.
-
-Usage:
-
-File path here
-
-Your file content here
-
-
-
-Example: Writing a configuration file
-
-frontend-config.json
-
-{
- "apiEndpoint": "https://api.example.com",
- "theme": {
- "primaryColor": "#007bff",
- "secondaryColor": "#6c757d",
- "fontFamily": "Arial, sans-serif"
- },
- "features": {
- "darkMode": true,
- "notifications": true,
- "analytics": false
- },
- "version": "1.0.0"
-}
-
-
-
-## ask_followup_question
-Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
-
-Parameters:
-- question: (required) A clear, specific question addressing the information needed
-- follow_up: (required) A list of 2-4 suggested answers, each in its own tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
-
-Usage:
-
-Your question here
-
-First suggestion
-Action with mode switch
-
-
-
-Example:
-
-What is the path to the frontend-config.json file?
-
-./src/frontend-config.json
-./config/frontend-config.json
-./frontend-config.json
-
-
-
-## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
-IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must confirm that you've received successful results from the user for any previous tool uses. If not, then DO NOT use this tool.
-Parameters:
-- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-Usage:
-
-
-Your final result description here
-
-
-
-Example: Requesting to attempt completion with a result
-
-
-I've updated the CSS
-
-
-
-## switch_mode
-Description: Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Code mode to make code changes. The user must approve the mode switch.
-Parameters:
-- mode_slug: (required) The slug of the mode to switch to (e.g., "code", "ask", "architect")
-- reason: (optional) The reason for switching modes
-Usage:
-
-Mode slug here
-Reason for switching here
-
-
-Example: Requesting to switch to code mode
-
-code
-Need to make code changes
-
-
-## new_task
-Description: This will let you create a new task instance in the chosen mode using your provided message.
-
-Parameters:
-- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
-- message: (required) The initial user message or instructions for this new task.
-
-Usage:
-
-your-mode-slug-here
-Your initial instructions here
-
-
-Example:
-
-code
-Implement a new feature for the application
-
-
-
-## update_todo_list
-
-**Description:**
-Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
-
-**Checklist Format:**
-- Use a single-level markdown checklist (no nesting or subtasks).
-- List todos in the intended execution order.
-- Status options:
- - [ ] Task description (pending)
- - [x] Task description (completed)
- - [-] Task description (in progress)
-
-**Status Rules:**
-- [ ] = pending (not started)
-- [x] = completed (fully finished, no unresolved issues)
-- [-] = in_progress (currently being worked on)
-
-**Core Principles:**
-- Before updating, always confirm which todos have been completed since the last update.
-- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
-- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
-- Do not remove any unfinished todos unless explicitly instructed.
-- Always retain all unfinished tasks, updating their status as needed.
-- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
-- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
-- Remove tasks only if they are no longer relevant or if the user requests deletion.
-
-**Usage Example:**
-
-
-[x] Analyze requirements
-[x] Design architecture
-[-] Implement core logic
-[ ] Write tests
-[ ] Update documentation
-
-
-
-*After completing "Implement core logic" and starting "Write tests":*
-
-
-[x] Analyze requirements
-[x] Design architecture
-[x] Implement core logic
-[-] Write tests
-[ ] Update documentation
-[ ] Add performance benchmarks
-
-
-
-**When to Use:**
-- The task is complicated or involves multiple steps or requires ongoing tracking.
-- You need to update the status of several todos at once.
-- New actionable items are discovered during task execution.
-- The user requests a todo list or provides multiple tasks.
-- The task is complex and benefits from clear, stepwise progress tracking.
-
-**When NOT to Use:**
-- There is only a single, trivial task.
-- The task can be completed in one or two simple steps.
-- The request is purely conversational or informational.
-
-**Task Management Guidelines:**
-- Mark task as completed immediately after all work of the current task is done.
-- Start the next task by marking it as in_progress.
-- Add new todos as soon as they are identified.
-- Use clear, descriptive task names.
-
-
-# Tool Use Guidelines
+ # Tool Use Guidelines
1. Assess what information you already have and what information you need to proceed with the task.
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like `ls` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
-4. Formulate your tool use using the XML format specified for each tool.
-5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
+4. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
- Information about whether the tool succeeded or failed, along with any reasons for failure.
- Linter errors that may have arisen due to the changes you made, which you'll need to address.
- New terminal output in reaction to the changes, which you may need to consider or act upon.
- Any other relevant feedback or information related to the tool use.
-6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
+5. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:
1. Confirm the success of each step before proceeding.
@@ -364,8 +32,6 @@ It is crucial to proceed step-by-step, waiting for the user's message after each
By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
-
-
====
CAPABILITIES
@@ -385,7 +51,7 @@ MODES
RULES
- The project base directory is: /test/path
-- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to .
+- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to execute_command.
- You cannot `cd` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path.
- Do not use the ~ character or $HOME to refer to the home directory.
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
@@ -434,7 +100,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
USER'S CUSTOM INSTRUCTIONS
-The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
+The following additional instructions are provided by the user, and should be followed to the best of your ability.
Language Preference:
You should always speak and think in the "en" language.
diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap
index 54df428abd..d32dc3dc23 100644
--- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap
+++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-true.snap
@@ -10,439 +10,19 @@ ALL responses MUST show ANY `language construct` OR filename reference as clicka
TOOL USE
-You have access to a set of tools that are executed upon the user's approval. You must use exactly one tool per message, and every assistant message must include a tool call. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
+You have access to a set of tools that are executed upon the user's approval. Use the provider-native tool-calling mechanism. Do not include XML markup or examples. You must use exactly one tool call per assistant response. Do not call zero tools or more than one tool in the same response.
-# Tool Use Formatting
-
-Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:
-
-
-value1
-value2
-...
-
-
-Always use the actual tool name as the XML tag name for proper parsing and execution.
-
-# Tools
-
-## read_file
-Description: Request to read the contents of one or more files. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when creating diffs or discussing code. Supports text extraction from PDF and DOCX files, but may not handle other binary files properly.
-
-**IMPORTANT: You can read a maximum of 5 files in a single request.** If you need to read more files, use multiple sequential read_file requests.
-
-
-Parameters:
-- args: Contains one or more file elements, where each file contains:
- - path: (required) File path (relative to workspace directory /test/path)
-
-
-Usage:
-
-
-
- path/to/file
-
-
-
-
-
-Examples:
-
-1. Reading a single file:
-
-
-
- src/app.ts
-
-
-
-
-
-2. Reading multiple files (within the 5-file limit):
-
-
-
- src/app.ts
-
-
-
- src/utils.ts
-
-
-
-
-
-3. Reading an entire file:
-
-
-
- config.json
-
-
-
-
-IMPORTANT: You MUST use this Efficient Reading Strategy:
-- You MUST read all related files and implementations together in a single operation (up to 5 files at once)
-- You MUST obtain all necessary context before proceeding with changes
-
-- When you need to read more than 5 files, prioritize the most critical files first, then use subsequent read_file requests for additional files
-
-## fetch_instructions
-Description: Request to fetch instructions to perform a task
-Parameters:
-- task: (required) The task to get instructions for. This can take the following values:
- create_mcp_server
- create_mode
-
-Example: Requesting instructions to create an MCP Server
-
-
-create_mcp_server
-
-
-## search_files
-Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
-
-Craft your regex patterns carefully to balance specificity and flexibility. Use this tool to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include surrounding context, so analyze the surrounding code to better understand the matches. Leverage this tool in combination with other tools for more comprehensive analysis - for example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches.
-
-Parameters:
-- path: (required) The path of the directory to search in (relative to the current workspace directory /test/path). This directory will be recursively searched.
-- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax.
-- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*).
-
-Usage:
-
-Directory path here
-Your regex pattern here
-file pattern here (optional)
-
-
-Example: Searching for all .ts files in the current directory
-
-.
-.*
-*.ts
-
-
-Example: Searching for function definitions in JavaScript files
-
-src
-function\s+\w+
-*.js
-
-
-## list_files
-Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.
-Parameters:
-- path: (required) The path of the directory to list contents for (relative to the current workspace directory /test/path)
-- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
-Usage:
-
-Directory path here
-true or false (optional)
-
-
-Example: Requesting to list all files in the current directory
-
-.
-false
-
-
-## apply_diff
-Description: Request to apply PRECISE, TARGETED modifications to an existing file by searching for specific sections of content and replacing them. This tool is for SURGICAL EDITS ONLY - specific changes to existing code.
-You can perform multiple distinct search and replace operations within a single `apply_diff` call by providing multiple SEARCH/REPLACE blocks in the `diff` parameter. This is the preferred way to make several targeted changes efficiently.
-The SEARCH section must exactly match existing content including whitespace and indentation.
-If you're not confident in the exact content to search for, use the read_file tool first to get the exact content.
-When applying the diffs, be extra careful to remember to change any closing brackets or other syntax that may be affected by the diff farther down in the file.
-ALWAYS make as many changes in a single 'apply_diff' request as possible using multiple SEARCH/REPLACE blocks
-
-Parameters:
-- path: (required) The path of the file to modify (relative to the current workspace directory /test/path)
-- diff: (required) The search/replace block defining the changes.
-
-Diff format:
-```
-<<<<<<< SEARCH
-:start_line: (required) The line number of original content where the search block starts.
--------
-[exact content to find including whitespace]
-=======
-[new content to replace with]
->>>>>>> REPLACE
-
-```
-
-
-Example:
-
-Original file:
-```
-1 | def calculate_total(items):
-2 | total = 0
-3 | for item in items:
-4 | total += item
-5 | return total
-```
-
-Search/Replace content:
-```
-<<<<<<< SEARCH
-:start_line:1
--------
-def calculate_total(items):
- total = 0
- for item in items:
- total += item
- return total
-=======
-def calculate_total(items):
- """Calculate total with 10% markup"""
- return sum(item * 1.1 for item in items)
->>>>>>> REPLACE
-
-```
-
-Search/Replace content with multiple edits:
-```
-<<<<<<< SEARCH
-:start_line:1
--------
-def calculate_total(items):
- sum = 0
-=======
-def calculate_sum(items):
- sum = 0
->>>>>>> REPLACE
-
-<<<<<<< SEARCH
-:start_line:4
--------
- total += item
- return total
-=======
- sum += item
- return sum
->>>>>>> REPLACE
-```
-
-
-Usage:
-
-File path here
-
-Your search/replace content here
-You can use multi search/replace block in one diff block, but make sure to include the line numbers for each block.
-Only use a single line of '=======' between search and replacement content, because multiple '=======' will corrupt the file.
-
-
-
-## write_to_file
-Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
-
-**Important:** You should prefer using other editing tools over write_to_file when making changes to existing files, since write_to_file is slower and cannot handle large files. Use write_to_file primarily for new file creation.
-
-When using this tool, use it directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code.
-
-When creating a new project, organize all new files within a dedicated project directory unless the user specifies otherwise. Structure the project logically, adhering to best practices for the specific type of project being created.
-
-Parameters:
-- path: (required) The path of the file to write to (relative to the current workspace directory /test/path)
-- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content.
-
-Usage:
-
-File path here
-
-Your file content here
-
-
-
-Example: Writing a configuration file
-
-frontend-config.json
-
-{
- "apiEndpoint": "https://api.example.com",
- "theme": {
- "primaryColor": "#007bff",
- "secondaryColor": "#6c757d",
- "fontFamily": "Arial, sans-serif"
- },
- "features": {
- "darkMode": true,
- "notifications": true,
- "analytics": false
- },
- "version": "1.0.0"
-}
-
-
-
-## ask_followup_question
-Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
-
-Parameters:
-- question: (required) A clear, specific question addressing the information needed
-- follow_up: (required) A list of 2-4 suggested answers, each in its own tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
-
-Usage:
-
-Your question here
-
-First suggestion
-Action with mode switch
-
-
-
-Example:
-
-What is the path to the frontend-config.json file?
-
-./src/frontend-config.json
-./config/frontend-config.json
-./frontend-config.json
-
-
-
-## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
-IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must confirm that you've received successful results from the user for any previous tool uses. If not, then DO NOT use this tool.
-Parameters:
-- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-Usage:
-
-
-Your final result description here
-
-
-
-Example: Requesting to attempt completion with a result
-
-
-I've updated the CSS
-
-
-
-## switch_mode
-Description: Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Code mode to make code changes. The user must approve the mode switch.
-Parameters:
-- mode_slug: (required) The slug of the mode to switch to (e.g., "code", "ask", "architect")
-- reason: (optional) The reason for switching modes
-Usage:
-
-Mode slug here
-Reason for switching here
-
-
-Example: Requesting to switch to code mode
-
-code
-Need to make code changes
-
-
-## new_task
-Description: This will let you create a new task instance in the chosen mode using your provided message.
-
-Parameters:
-- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
-- message: (required) The initial user message or instructions for this new task.
-
-Usage:
-
-your-mode-slug-here
-Your initial instructions here
-
-
-Example:
-
-code
-Implement a new feature for the application
-
-
-
-## update_todo_list
-
-**Description:**
-Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
-
-**Checklist Format:**
-- Use a single-level markdown checklist (no nesting or subtasks).
-- List todos in the intended execution order.
-- Status options:
- - [ ] Task description (pending)
- - [x] Task description (completed)
- - [-] Task description (in progress)
-
-**Status Rules:**
-- [ ] = pending (not started)
-- [x] = completed (fully finished, no unresolved issues)
-- [-] = in_progress (currently being worked on)
-
-**Core Principles:**
-- Before updating, always confirm which todos have been completed since the last update.
-- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
-- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
-- Do not remove any unfinished todos unless explicitly instructed.
-- Always retain all unfinished tasks, updating their status as needed.
-- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
-- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
-- Remove tasks only if they are no longer relevant or if the user requests deletion.
-
-**Usage Example:**
-
-
-[x] Analyze requirements
-[x] Design architecture
-[-] Implement core logic
-[ ] Write tests
-[ ] Update documentation
-
-
-
-*After completing "Implement core logic" and starting "Write tests":*
-
-
-[x] Analyze requirements
-[x] Design architecture
-[x] Implement core logic
-[-] Write tests
-[ ] Update documentation
-[ ] Add performance benchmarks
-
-
-
-**When to Use:**
-- The task is complicated or involves multiple steps or requires ongoing tracking.
-- You need to update the status of several todos at once.
-- New actionable items are discovered during task execution.
-- The user requests a todo list or provides multiple tasks.
-- The task is complex and benefits from clear, stepwise progress tracking.
-
-**When NOT to Use:**
-- There is only a single, trivial task.
-- The task can be completed in one or two simple steps.
-- The request is purely conversational or informational.
-
-**Task Management Guidelines:**
-- Mark task as completed immediately after all work of the current task is done.
-- Start the next task by marking it as in_progress.
-- Add new todos as soon as they are identified.
-- Use clear, descriptive task names.
-
-
-# Tool Use Guidelines
+ # Tool Use Guidelines
1. Assess what information you already have and what information you need to proceed with the task.
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like `ls` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
-4. Formulate your tool use using the XML format specified for each tool.
-5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
+4. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
- Information about whether the tool succeeded or failed, along with any reasons for failure.
- Linter errors that may have arisen due to the changes you made, which you'll need to address.
- New terminal output in reaction to the changes, which you may need to consider or act upon.
- Any other relevant feedback or information related to the tool use.
-6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
+5. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:
1. Confirm the success of each step before proceeding.
@@ -452,8 +32,6 @@ It is crucial to proceed step-by-step, waiting for the user's message after each
By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
-
-
====
CAPABILITIES
@@ -473,7 +51,7 @@ MODES
RULES
- The project base directory is: /test/path
-- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to .
+- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to execute_command.
- You cannot `cd` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path.
- Do not use the ~ character or $HOME to refer to the home directory.
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
@@ -522,7 +100,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
USER'S CUSTOM INSTRUCTIONS
-The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
+The following additional instructions are provided by the user, and should be followed to the best of your ability.
Language Preference:
You should always speak and think in the "en" language.
diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap
index ee8a50e993..d32dc3dc23 100644
--- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap
+++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-diff-enabled-undefined.snap
@@ -10,351 +10,19 @@ ALL responses MUST show ANY `language construct` OR filename reference as clicka
TOOL USE
-You have access to a set of tools that are executed upon the user's approval. You must use exactly one tool per message, and every assistant message must include a tool call. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
+You have access to a set of tools that are executed upon the user's approval. Use the provider-native tool-calling mechanism. Do not include XML markup or examples. You must use exactly one tool call per assistant response. Do not call zero tools or more than one tool in the same response.
-# Tool Use Formatting
-
-Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:
-
-
-value1
-value2
-...
-
-
-Always use the actual tool name as the XML tag name for proper parsing and execution.
-
-# Tools
-
-## read_file
-Description: Request to read the contents of one or more files. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when creating diffs or discussing code. Supports text extraction from PDF and DOCX files, but may not handle other binary files properly.
-
-**IMPORTANT: You can read a maximum of 5 files in a single request.** If you need to read more files, use multiple sequential read_file requests.
-
-
-Parameters:
-- args: Contains one or more file elements, where each file contains:
- - path: (required) File path (relative to workspace directory /test/path)
-
-
-Usage:
-
-
-
- path/to/file
-
-
-
-
-
-Examples:
-
-1. Reading a single file:
-
-
-
- src/app.ts
-
-
-
-
-
-2. Reading multiple files (within the 5-file limit):
-
-
-
- src/app.ts
-
-
-
- src/utils.ts
-
-
-
-
-
-3. Reading an entire file:
-
-
-
- config.json
-
-
-
-
-IMPORTANT: You MUST use this Efficient Reading Strategy:
-- You MUST read all related files and implementations together in a single operation (up to 5 files at once)
-- You MUST obtain all necessary context before proceeding with changes
-
-- When you need to read more than 5 files, prioritize the most critical files first, then use subsequent read_file requests for additional files
-
-## fetch_instructions
-Description: Request to fetch instructions to perform a task
-Parameters:
-- task: (required) The task to get instructions for. This can take the following values:
- create_mcp_server
- create_mode
-
-Example: Requesting instructions to create an MCP Server
-
-
-create_mcp_server
-
-
-## search_files
-Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
-
-Craft your regex patterns carefully to balance specificity and flexibility. Use this tool to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include surrounding context, so analyze the surrounding code to better understand the matches. Leverage this tool in combination with other tools for more comprehensive analysis - for example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches.
-
-Parameters:
-- path: (required) The path of the directory to search in (relative to the current workspace directory /test/path). This directory will be recursively searched.
-- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax.
-- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*).
-
-Usage:
-
-Directory path here
-Your regex pattern here
-file pattern here (optional)
-
-
-Example: Searching for all .ts files in the current directory
-
-.
-.*
-*.ts
-
-
-Example: Searching for function definitions in JavaScript files
-
-src
-function\s+\w+
-*.js
-
-
-## list_files
-Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.
-Parameters:
-- path: (required) The path of the directory to list contents for (relative to the current workspace directory /test/path)
-- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
-Usage:
-
-Directory path here
-true or false (optional)
-
-
-Example: Requesting to list all files in the current directory
-
-.
-false
-
-
-## write_to_file
-Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
-
-**Important:** You should prefer using other editing tools over write_to_file when making changes to existing files, since write_to_file is slower and cannot handle large files. Use write_to_file primarily for new file creation.
-
-When using this tool, use it directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code.
-
-When creating a new project, organize all new files within a dedicated project directory unless the user specifies otherwise. Structure the project logically, adhering to best practices for the specific type of project being created.
-
-Parameters:
-- path: (required) The path of the file to write to (relative to the current workspace directory /test/path)
-- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content.
-
-Usage:
-
-File path here
-
-Your file content here
-
-
-
-Example: Writing a configuration file
-
-frontend-config.json
-
-{
- "apiEndpoint": "https://api.example.com",
- "theme": {
- "primaryColor": "#007bff",
- "secondaryColor": "#6c757d",
- "fontFamily": "Arial, sans-serif"
- },
- "features": {
- "darkMode": true,
- "notifications": true,
- "analytics": false
- },
- "version": "1.0.0"
-}
-
-
-
-## ask_followup_question
-Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
-
-Parameters:
-- question: (required) A clear, specific question addressing the information needed
-- follow_up: (required) A list of 2-4 suggested answers, each in its own tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
-
-Usage:
-
-Your question here
-
-First suggestion
-Action with mode switch
-
-
-
-Example:
-
-What is the path to the frontend-config.json file?
-
-./src/frontend-config.json
-./config/frontend-config.json
-./frontend-config.json
-
-
-
-## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
-IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must confirm that you've received successful results from the user for any previous tool uses. If not, then DO NOT use this tool.
-Parameters:
-- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-Usage:
-
-
-Your final result description here
-
-
-
-Example: Requesting to attempt completion with a result
-
-
-I've updated the CSS
-
-
-
-## switch_mode
-Description: Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Code mode to make code changes. The user must approve the mode switch.
-Parameters:
-- mode_slug: (required) The slug of the mode to switch to (e.g., "code", "ask", "architect")
-- reason: (optional) The reason for switching modes
-Usage:
-
-Mode slug here
-Reason for switching here
-
-
-Example: Requesting to switch to code mode
-
-code
-Need to make code changes
-
-
-## new_task
-Description: This will let you create a new task instance in the chosen mode using your provided message.
-
-Parameters:
-- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
-- message: (required) The initial user message or instructions for this new task.
-
-Usage:
-
-your-mode-slug-here
-Your initial instructions here
-
-
-Example:
-
-code
-Implement a new feature for the application
-
-
-
-## update_todo_list
-
-**Description:**
-Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
-
-**Checklist Format:**
-- Use a single-level markdown checklist (no nesting or subtasks).
-- List todos in the intended execution order.
-- Status options:
- - [ ] Task description (pending)
- - [x] Task description (completed)
- - [-] Task description (in progress)
-
-**Status Rules:**
-- [ ] = pending (not started)
-- [x] = completed (fully finished, no unresolved issues)
-- [-] = in_progress (currently being worked on)
-
-**Core Principles:**
-- Before updating, always confirm which todos have been completed since the last update.
-- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
-- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
-- Do not remove any unfinished todos unless explicitly instructed.
-- Always retain all unfinished tasks, updating their status as needed.
-- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
-- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
-- Remove tasks only if they are no longer relevant or if the user requests deletion.
-
-**Usage Example:**
-
-
-[x] Analyze requirements
-[x] Design architecture
-[-] Implement core logic
-[ ] Write tests
-[ ] Update documentation
-
-
-
-*After completing "Implement core logic" and starting "Write tests":*
-
-
-[x] Analyze requirements
-[x] Design architecture
-[x] Implement core logic
-[-] Write tests
-[ ] Update documentation
-[ ] Add performance benchmarks
-
-
-
-**When to Use:**
-- The task is complicated or involves multiple steps or requires ongoing tracking.
-- You need to update the status of several todos at once.
-- New actionable items are discovered during task execution.
-- The user requests a todo list or provides multiple tasks.
-- The task is complex and benefits from clear, stepwise progress tracking.
-
-**When NOT to Use:**
-- There is only a single, trivial task.
-- The task can be completed in one or two simple steps.
-- The request is purely conversational or informational.
-
-**Task Management Guidelines:**
-- Mark task as completed immediately after all work of the current task is done.
-- Start the next task by marking it as in_progress.
-- Add new todos as soon as they are identified.
-- Use clear, descriptive task names.
-
-
-# Tool Use Guidelines
+ # Tool Use Guidelines
1. Assess what information you already have and what information you need to proceed with the task.
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like `ls` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
-4. Formulate your tool use using the XML format specified for each tool.
-5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
+4. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
- Information about whether the tool succeeded or failed, along with any reasons for failure.
- Linter errors that may have arisen due to the changes you made, which you'll need to address.
- New terminal output in reaction to the changes, which you may need to consider or act upon.
- Any other relevant feedback or information related to the tool use.
-6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
+5. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:
1. Confirm the success of each step before proceeding.
@@ -364,8 +32,6 @@ It is crucial to proceed step-by-step, waiting for the user's message after each
By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
-
-
====
CAPABILITIES
@@ -385,7 +51,7 @@ MODES
RULES
- The project base directory is: /test/path
-- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to .
+- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to execute_command.
- You cannot `cd` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path.
- Do not use the ~ character or $HOME to refer to the home directory.
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
@@ -434,7 +100,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
USER'S CUSTOM INSTRUCTIONS
-The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
+The following additional instructions are provided by the user, and should be followed to the best of your ability.
Language Preference:
You should always speak and think in the "en" language.
diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap
index ee8a50e993..42e8bba9c6 100644
--- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap
+++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-different-viewport-size.snap
@@ -10,361 +10,15 @@ ALL responses MUST show ANY `language construct` OR filename reference as clicka
TOOL USE
-You have access to a set of tools that are executed upon the user's approval. You must use exactly one tool per message, and every assistant message must include a tool call. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
+You have access to a set of tools that are executed upon the user's approval. Use the provider-native tool-calling mechanism. Do not include XML markup or examples. You must call at least one tool per assistant response. Prefer calling as many tools as are reasonably needed in a single response to reduce back-and-forth and complete tasks faster.
-# Tool Use Formatting
-
-Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:
-
-
-value1
-value2
-...
-
-
-Always use the actual tool name as the XML tag name for proper parsing and execution.
-
-# Tools
-
-## read_file
-Description: Request to read the contents of one or more files. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when creating diffs or discussing code. Supports text extraction from PDF and DOCX files, but may not handle other binary files properly.
-
-**IMPORTANT: You can read a maximum of 5 files in a single request.** If you need to read more files, use multiple sequential read_file requests.
-
-
-Parameters:
-- args: Contains one or more file elements, where each file contains:
- - path: (required) File path (relative to workspace directory /test/path)
-
-
-Usage:
-
-
-
- path/to/file
-
-
-
-
-
-Examples:
-
-1. Reading a single file:
-
-
-
- src/app.ts
-
-
-
-
-
-2. Reading multiple files (within the 5-file limit):
-
-
-
- src/app.ts
-
-
-
- src/utils.ts
-
-
-
-
-
-3. Reading an entire file:
-
-
-
- config.json
-
-
-
-
-IMPORTANT: You MUST use this Efficient Reading Strategy:
-- You MUST read all related files and implementations together in a single operation (up to 5 files at once)
-- You MUST obtain all necessary context before proceeding with changes
-
-- When you need to read more than 5 files, prioritize the most critical files first, then use subsequent read_file requests for additional files
-
-## fetch_instructions
-Description: Request to fetch instructions to perform a task
-Parameters:
-- task: (required) The task to get instructions for. This can take the following values:
- create_mcp_server
- create_mode
-
-Example: Requesting instructions to create an MCP Server
-
-
-create_mcp_server
-
-
-## search_files
-Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
-
-Craft your regex patterns carefully to balance specificity and flexibility. Use this tool to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include surrounding context, so analyze the surrounding code to better understand the matches. Leverage this tool in combination with other tools for more comprehensive analysis - for example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches.
-
-Parameters:
-- path: (required) The path of the directory to search in (relative to the current workspace directory /test/path). This directory will be recursively searched.
-- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax.
-- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*).
-
-Usage:
-
-Directory path here
-Your regex pattern here
-file pattern here (optional)
-
-
-Example: Searching for all .ts files in the current directory
-
-.
-.*
-*.ts
-
-
-Example: Searching for function definitions in JavaScript files
-
-src
-function\s+\w+
-*.js
-
-
-## list_files
-Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.
-Parameters:
-- path: (required) The path of the directory to list contents for (relative to the current workspace directory /test/path)
-- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
-Usage:
-
-Directory path here
-true or false (optional)
-
-
-Example: Requesting to list all files in the current directory
-
-.
-false
-
-
-## write_to_file
-Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
-
-**Important:** You should prefer using other editing tools over write_to_file when making changes to existing files, since write_to_file is slower and cannot handle large files. Use write_to_file primarily for new file creation.
-
-When using this tool, use it directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code.
-
-When creating a new project, organize all new files within a dedicated project directory unless the user specifies otherwise. Structure the project logically, adhering to best practices for the specific type of project being created.
-
-Parameters:
-- path: (required) The path of the file to write to (relative to the current workspace directory /test/path)
-- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content.
-
-Usage:
-
-File path here
-
-Your file content here
-
-
-
-Example: Writing a configuration file
-
-frontend-config.json
-
-{
- "apiEndpoint": "https://api.example.com",
- "theme": {
- "primaryColor": "#007bff",
- "secondaryColor": "#6c757d",
- "fontFamily": "Arial, sans-serif"
- },
- "features": {
- "darkMode": true,
- "notifications": true,
- "analytics": false
- },
- "version": "1.0.0"
-}
-
-
-
-## ask_followup_question
-Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
-
-Parameters:
-- question: (required) A clear, specific question addressing the information needed
-- follow_up: (required) A list of 2-4 suggested answers, each in its own tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
-
-Usage:
-
-Your question here
-
-First suggestion
-Action with mode switch
-
-
-
-Example:
-
-What is the path to the frontend-config.json file?
-
-./src/frontend-config.json
-./config/frontend-config.json
-./frontend-config.json
-
-
-
-## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
-IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must confirm that you've received successful results from the user for any previous tool uses. If not, then DO NOT use this tool.
-Parameters:
-- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-Usage:
-
-
-Your final result description here
-
-
-
-Example: Requesting to attempt completion with a result
-
-
-I've updated the CSS
-
-
-
-## switch_mode
-Description: Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Code mode to make code changes. The user must approve the mode switch.
-Parameters:
-- mode_slug: (required) The slug of the mode to switch to (e.g., "code", "ask", "architect")
-- reason: (optional) The reason for switching modes
-Usage:
-
-Mode slug here
-Reason for switching here
-
-
-Example: Requesting to switch to code mode
-
-code
-Need to make code changes
-
-
-## new_task
-Description: This will let you create a new task instance in the chosen mode using your provided message.
-
-Parameters:
-- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
-- message: (required) The initial user message or instructions for this new task.
-
-Usage:
-
-your-mode-slug-here
-Your initial instructions here
-
-
-Example:
-
-code
-Implement a new feature for the application
-
-
-
-## update_todo_list
-
-**Description:**
-Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
-
-**Checklist Format:**
-- Use a single-level markdown checklist (no nesting or subtasks).
-- List todos in the intended execution order.
-- Status options:
- - [ ] Task description (pending)
- - [x] Task description (completed)
- - [-] Task description (in progress)
-
-**Status Rules:**
-- [ ] = pending (not started)
-- [x] = completed (fully finished, no unresolved issues)
-- [-] = in_progress (currently being worked on)
-
-**Core Principles:**
-- Before updating, always confirm which todos have been completed since the last update.
-- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
-- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
-- Do not remove any unfinished todos unless explicitly instructed.
-- Always retain all unfinished tasks, updating their status as needed.
-- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
-- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
-- Remove tasks only if they are no longer relevant or if the user requests deletion.
-
-**Usage Example:**
-
-
-[x] Analyze requirements
-[x] Design architecture
-[-] Implement core logic
-[ ] Write tests
-[ ] Update documentation
-
-
-
-*After completing "Implement core logic" and starting "Write tests":*
-
-
-[x] Analyze requirements
-[x] Design architecture
-[x] Implement core logic
-[-] Write tests
-[ ] Update documentation
-[ ] Add performance benchmarks
-
-
-
-**When to Use:**
-- The task is complicated or involves multiple steps or requires ongoing tracking.
-- You need to update the status of several todos at once.
-- New actionable items are discovered during task execution.
-- The user requests a todo list or provides multiple tasks.
-- The task is complex and benefits from clear, stepwise progress tracking.
-
-**When NOT to Use:**
-- There is only a single, trivial task.
-- The task can be completed in one or two simple steps.
-- The request is purely conversational or informational.
-
-**Task Management Guidelines:**
-- Mark task as completed immediately after all work of the current task is done.
-- Start the next task by marking it as in_progress.
-- Add new todos as soon as they are identified.
-- Use clear, descriptive task names.
-
-
-# Tool Use Guidelines
+ # Tool Use Guidelines
1. Assess what information you already have and what information you need to proceed with the task.
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like `ls` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
-3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
-4. Formulate your tool use using the XML format specified for each tool.
-5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
- - Information about whether the tool succeeded or failed, along with any reasons for failure.
- - Linter errors that may have arisen due to the changes you made, which you'll need to address.
- - New terminal output in reaction to the changes, which you may need to consider or act upon.
- - Any other relevant feedback or information related to the tool use.
-6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
-
-It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:
-1. Confirm the success of each step before proceeding.
-2. Address any issues or errors that arise immediately.
-3. Adapt your approach based on new information or unexpected results.
-4. Ensure that each action builds correctly on the previous ones.
-
-By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
-
+3. If multiple actions are needed, you may use multiple tools in a single message when appropriate, or use tools iteratively across messages. Each tool use should be informed by the results of previous tool uses. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
+By carefully considering the user's response after tool executions, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
====
@@ -385,7 +39,7 @@ MODES
RULES
- The project base directory is: /test/path
-- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to .
+- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to execute_command.
- You cannot `cd` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path.
- Do not use the ~ character or $HOME to refer to the home directory.
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
@@ -434,7 +88,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
USER'S CUSTOM INSTRUCTIONS
-The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
+The following additional instructions are provided by the user, and should be followed to the best of your ability.
Language Preference:
You should always speak and think in the "en" language.
diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap
index acc36d1ffd..5aa6677ab0 100644
--- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap
+++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-mcp-hub-provided.snap
@@ -10,427 +10,15 @@ ALL responses MUST show ANY `language construct` OR filename reference as clicka
TOOL USE
-You have access to a set of tools that are executed upon the user's approval. You must use exactly one tool per message, and every assistant message must include a tool call. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
+You have access to a set of tools that are executed upon the user's approval. Use the provider-native tool-calling mechanism. Do not include XML markup or examples. You must call at least one tool per assistant response. Prefer calling as many tools as are reasonably needed in a single response to reduce back-and-forth and complete tasks faster.
-# Tool Use Formatting
-
-Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:
-
-
-value1
-value2
-...
-
-
-Always use the actual tool name as the XML tag name for proper parsing and execution.
-
-# Tools
-
-## read_file
-Description: Request to read the contents of one or more files. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when creating diffs or discussing code. Supports text extraction from PDF and DOCX files, but may not handle other binary files properly.
-
-**IMPORTANT: You can read a maximum of 5 files in a single request.** If you need to read more files, use multiple sequential read_file requests.
-
-
-Parameters:
-- args: Contains one or more file elements, where each file contains:
- - path: (required) File path (relative to workspace directory /test/path)
-
-
-Usage:
-
-
-
- path/to/file
-
-
-
-
-
-Examples:
-
-1. Reading a single file:
-
-
-
- src/app.ts
-
-
-
-
-
-2. Reading multiple files (within the 5-file limit):
-
-
-
- src/app.ts
-
-
-
- src/utils.ts
-
-
-
-
-
-3. Reading an entire file:
-
-
-
- config.json
-
-
-
-
-IMPORTANT: You MUST use this Efficient Reading Strategy:
-- You MUST read all related files and implementations together in a single operation (up to 5 files at once)
-- You MUST obtain all necessary context before proceeding with changes
-
-- When you need to read more than 5 files, prioritize the most critical files first, then use subsequent read_file requests for additional files
-
-## fetch_instructions
-Description: Request to fetch instructions to perform a task
-Parameters:
-- task: (required) The task to get instructions for. This can take the following values:
- create_mcp_server
- create_mode
-
-Example: Requesting instructions to create an MCP Server
-
-
-create_mcp_server
-
-
-## search_files
-Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
-
-Craft your regex patterns carefully to balance specificity and flexibility. Use this tool to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include surrounding context, so analyze the surrounding code to better understand the matches. Leverage this tool in combination with other tools for more comprehensive analysis - for example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches.
-
-Parameters:
-- path: (required) The path of the directory to search in (relative to the current workspace directory /test/path). This directory will be recursively searched.
-- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax.
-- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*).
-
-Usage:
-
-Directory path here
-Your regex pattern here
-file pattern here (optional)
-
-
-Example: Searching for all .ts files in the current directory
-
-.
-.*
-*.ts
-
-
-Example: Searching for function definitions in JavaScript files
-
-src
-function\s+\w+
-*.js
-
-
-## list_files
-Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.
-Parameters:
-- path: (required) The path of the directory to list contents for (relative to the current workspace directory /test/path)
-- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
-Usage:
-
-Directory path here
-true or false (optional)
-
-
-Example: Requesting to list all files in the current directory
-
-.
-false
-
-
-## write_to_file
-Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
-
-**Important:** You should prefer using other editing tools over write_to_file when making changes to existing files, since write_to_file is slower and cannot handle large files. Use write_to_file primarily for new file creation.
-
-When using this tool, use it directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code.
-
-When creating a new project, organize all new files within a dedicated project directory unless the user specifies otherwise. Structure the project logically, adhering to best practices for the specific type of project being created.
-
-Parameters:
-- path: (required) The path of the file to write to (relative to the current workspace directory /test/path)
-- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content.
-
-Usage:
-
-File path here
-
-Your file content here
-
-
-
-Example: Writing a configuration file
-
-frontend-config.json
-
-{
- "apiEndpoint": "https://api.example.com",
- "theme": {
- "primaryColor": "#007bff",
- "secondaryColor": "#6c757d",
- "fontFamily": "Arial, sans-serif"
- },
- "features": {
- "darkMode": true,
- "notifications": true,
- "analytics": false
- },
- "version": "1.0.0"
-}
-
-
-
-## use_mcp_tool
-Description: Request to use a tool provided by a connected MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters.
-Parameters:
-- server_name: (required) The name of the MCP server providing the tool
-- tool_name: (required) The name of the tool to execute
-- arguments: (required) A JSON object containing the tool's input parameters, following the tool's input schema
-Usage:
-
-server name here
-tool name here
-
-{
- "param1": "value1",
- "param2": "value2"
-}
-
-
-
-Example: Requesting to use an MCP tool
-
-
-weather-server
-get_forecast
-
-{
- "city": "San Francisco",
- "days": 5
-}
-
-
-
-## access_mcp_resource
-Description: Request to access a resource provided by a connected MCP server. Resources represent data sources that can be used as context, such as files, API responses, or system information.
-Parameters:
-- server_name: (required) The name of the MCP server providing the resource
-- uri: (required) The URI identifying the specific resource to access
-Usage:
-
-server name here
-resource URI here
-
-
-Example: Requesting to access an MCP resource
-
-
-weather-server
-weather://san-francisco/current
-
-
-## ask_followup_question
-Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
-
-Parameters:
-- question: (required) A clear, specific question addressing the information needed
-- follow_up: (required) A list of 2-4 suggested answers, each in its own tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
-
-Usage:
-
-Your question here
-
-First suggestion
-Action with mode switch
-
-
-
-Example:
-
-What is the path to the frontend-config.json file?
-
-./src/frontend-config.json
-./config/frontend-config.json
-./frontend-config.json
-
-
-
-## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
-IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must confirm that you've received successful results from the user for any previous tool uses. If not, then DO NOT use this tool.
-Parameters:
-- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-Usage:
-
-
-Your final result description here
-
-
-
-Example: Requesting to attempt completion with a result
-
-
-I've updated the CSS
-
-
-
-## switch_mode
-Description: Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Code mode to make code changes. The user must approve the mode switch.
-Parameters:
-- mode_slug: (required) The slug of the mode to switch to (e.g., "code", "ask", "architect")
-- reason: (optional) The reason for switching modes
-Usage:
-
-Mode slug here
-Reason for switching here
-
-
-Example: Requesting to switch to code mode
-
-code
-Need to make code changes
-
-
-## new_task
-Description: This will let you create a new task instance in the chosen mode using your provided message.
-
-Parameters:
-- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
-- message: (required) The initial user message or instructions for this new task.
-
-Usage:
-
-your-mode-slug-here
-Your initial instructions here
-
-
-Example:
-
-code
-Implement a new feature for the application
-
-
-
-## update_todo_list
-
-**Description:**
-Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
-
-**Checklist Format:**
-- Use a single-level markdown checklist (no nesting or subtasks).
-- List todos in the intended execution order.
-- Status options:
- - [ ] Task description (pending)
- - [x] Task description (completed)
- - [-] Task description (in progress)
-
-**Status Rules:**
-- [ ] = pending (not started)
-- [x] = completed (fully finished, no unresolved issues)
-- [-] = in_progress (currently being worked on)
-
-**Core Principles:**
-- Before updating, always confirm which todos have been completed since the last update.
-- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
-- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
-- Do not remove any unfinished todos unless explicitly instructed.
-- Always retain all unfinished tasks, updating their status as needed.
-- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
-- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
-- Remove tasks only if they are no longer relevant or if the user requests deletion.
-
-**Usage Example:**
-
-
-[x] Analyze requirements
-[x] Design architecture
-[-] Implement core logic
-[ ] Write tests
-[ ] Update documentation
-
-
-
-*After completing "Implement core logic" and starting "Write tests":*
-
-
-[x] Analyze requirements
-[x] Design architecture
-[x] Implement core logic
-[-] Write tests
-[ ] Update documentation
-[ ] Add performance benchmarks
-
-
-
-**When to Use:**
-- The task is complicated or involves multiple steps or requires ongoing tracking.
-- You need to update the status of several todos at once.
-- New actionable items are discovered during task execution.
-- The user requests a todo list or provides multiple tasks.
-- The task is complex and benefits from clear, stepwise progress tracking.
-
-**When NOT to Use:**
-- There is only a single, trivial task.
-- The task can be completed in one or two simple steps.
-- The request is purely conversational or informational.
-
-**Task Management Guidelines:**
-- Mark task as completed immediately after all work of the current task is done.
-- Start the next task by marking it as in_progress.
-- Add new todos as soon as they are identified.
-- Use clear, descriptive task names.
-
-
-# Tool Use Guidelines
+ # Tool Use Guidelines
1. Assess what information you already have and what information you need to proceed with the task.
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like `ls` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
-3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
-4. Formulate your tool use using the XML format specified for each tool.
-5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
- - Information about whether the tool succeeded or failed, along with any reasons for failure.
- - Linter errors that may have arisen due to the changes you made, which you'll need to address.
- - New terminal output in reaction to the changes, which you may need to consider or act upon.
- - Any other relevant feedback or information related to the tool use.
-6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
+3. If multiple actions are needed, you may use multiple tools in a single message when appropriate, or use tools iteratively across messages. Each tool use should be informed by the results of previous tool uses. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
-It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:
-1. Confirm the success of each step before proceeding.
-2. Address any issues or errors that arise immediately.
-3. Adapt your approach based on new information or unexpected results.
-4. Ensure that each action builds correctly on the previous ones.
-
-By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
-
-MCP SERVERS
-
-The Model Context Protocol (MCP) enables communication between the system and MCP servers that provide additional tools and resources to extend your capabilities. MCP servers can be one of two types:
-
-1. Local (Stdio-based) servers: These run locally on the user's machine and communicate via standard input/output
-2. Remote (SSE-based) servers: These run on remote machines and communicate via Server-Sent Events (SSE) over HTTP/HTTPS
-
-# Connected MCP Servers
-
-When a server is connected, you can use the server's tools via the `use_mcp_tool` tool, and access the server's resources via the `access_mcp_resource` tool.
-
-
-## Creating an MCP Server
-
-The user may ask you something along the lines of "add a tool" that does some function, in other words to create an MCP server that provides tools and resources that may connect to external APIs for example. If they do, you should obtain detailed instructions on this topic using the fetch_instructions tool, like this:
-
-create_mcp_server
-
+By carefully considering the user's response after tool executions, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
====
@@ -453,7 +41,7 @@ MODES
RULES
- The project base directory is: /test/path
-- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to .
+- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to execute_command.
- You cannot `cd` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path.
- Do not use the ~ character or $HOME to refer to the home directory.
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
@@ -502,7 +90,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
USER'S CUSTOM INSTRUCTIONS
-The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
+The following additional instructions are provided by the user, and should be followed to the best of your ability.
Language Preference:
You should always speak and think in the "en" language.
diff --git a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap
index ee8a50e993..42e8bba9c6 100644
--- a/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap
+++ b/src/core/prompts/__tests__/__snapshots__/system-prompt/with-undefined-mcp-hub.snap
@@ -10,361 +10,15 @@ ALL responses MUST show ANY `language construct` OR filename reference as clicka
TOOL USE
-You have access to a set of tools that are executed upon the user's approval. You must use exactly one tool per message, and every assistant message must include a tool call. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
+You have access to a set of tools that are executed upon the user's approval. Use the provider-native tool-calling mechanism. Do not include XML markup or examples. You must call at least one tool per assistant response. Prefer calling as many tools as are reasonably needed in a single response to reduce back-and-forth and complete tasks faster.
-# Tool Use Formatting
-
-Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:
-
-
-value1
-value2
-...
-
-
-Always use the actual tool name as the XML tag name for proper parsing and execution.
-
-# Tools
-
-## read_file
-Description: Request to read the contents of one or more files. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when creating diffs or discussing code. Supports text extraction from PDF and DOCX files, but may not handle other binary files properly.
-
-**IMPORTANT: You can read a maximum of 5 files in a single request.** If you need to read more files, use multiple sequential read_file requests.
-
-
-Parameters:
-- args: Contains one or more file elements, where each file contains:
- - path: (required) File path (relative to workspace directory /test/path)
-
-
-Usage:
-
-
-
- path/to/file
-
-
-
-
-
-Examples:
-
-1. Reading a single file:
-
-
-
- src/app.ts
-
-
-
-
-
-2. Reading multiple files (within the 5-file limit):
-
-
-
- src/app.ts
-
-
-
- src/utils.ts
-
-
-
-
-
-3. Reading an entire file:
-
-
-
- config.json
-
-
-
-
-IMPORTANT: You MUST use this Efficient Reading Strategy:
-- You MUST read all related files and implementations together in a single operation (up to 5 files at once)
-- You MUST obtain all necessary context before proceeding with changes
-
-- When you need to read more than 5 files, prioritize the most critical files first, then use subsequent read_file requests for additional files
-
-## fetch_instructions
-Description: Request to fetch instructions to perform a task
-Parameters:
-- task: (required) The task to get instructions for. This can take the following values:
- create_mcp_server
- create_mode
-
-Example: Requesting instructions to create an MCP Server
-
-
-create_mcp_server
-
-
-## search_files
-Description: Request to perform a regex search across files in a specified directory, providing context-rich results. This tool searches for patterns or specific content across multiple files, displaying each match with encapsulating context.
-
-Craft your regex patterns carefully to balance specificity and flexibility. Use this tool to find code patterns, TODO comments, function definitions, or any text-based information across the project. The results include surrounding context, so analyze the surrounding code to better understand the matches. Leverage this tool in combination with other tools for more comprehensive analysis - for example, use it to find specific code patterns, then use read_file to examine the full context of interesting matches.
-
-Parameters:
-- path: (required) The path of the directory to search in (relative to the current workspace directory /test/path). This directory will be recursively searched.
-- regex: (required) The regular expression pattern to search for. Uses Rust regex syntax.
-- file_pattern: (optional) Glob pattern to filter files (e.g., '*.ts' for TypeScript files). If not provided, it will search all files (*).
-
-Usage:
-
-Directory path here
-Your regex pattern here
-file pattern here (optional)
-
-
-Example: Searching for all .ts files in the current directory
-
-.
-.*
-*.ts
-
-
-Example: Searching for function definitions in JavaScript files
-
-src
-function\s+\w+
-*.js
-
-
-## list_files
-Description: Request to list files and directories within the specified directory. If recursive is true, it will list all files and directories recursively. If recursive is false or not provided, it will only list the top-level contents. Do not use this tool to confirm the existence of files you may have created, as the user will let you know if the files were created successfully or not.
-Parameters:
-- path: (required) The path of the directory to list contents for (relative to the current workspace directory /test/path)
-- recursive: (optional) Whether to list files recursively. Use true for recursive listing, false or omit for top-level only.
-Usage:
-
-Directory path here
-true or false (optional)
-
-
-Example: Requesting to list all files in the current directory
-
-.
-false
-
-
-## write_to_file
-Description: Request to write content to a file. This tool is primarily used for **creating new files** or for scenarios where a **complete rewrite of an existing file is intentionally required**. If the file exists, it will be overwritten. If it doesn't exist, it will be created. This tool will automatically create any directories needed to write the file.
-
-**Important:** You should prefer using other editing tools over write_to_file when making changes to existing files, since write_to_file is slower and cannot handle large files. Use write_to_file primarily for new file creation.
-
-When using this tool, use it directly with the desired content. You do not need to display the content before using the tool. ALWAYS provide the COMPLETE file content in your response. This is NON-NEGOTIABLE. Partial updates or placeholders like '// rest of code unchanged' are STRICTLY FORBIDDEN. You MUST include ALL parts of the file, even if they haven't been modified. Failure to do so will result in incomplete or broken code.
-
-When creating a new project, organize all new files within a dedicated project directory unless the user specifies otherwise. Structure the project logically, adhering to best practices for the specific type of project being created.
-
-Parameters:
-- path: (required) The path of the file to write to (relative to the current workspace directory /test/path)
-- content: (required) The content to write to the file. ALWAYS provide the COMPLETE intended content of the file, without any truncation or omissions. You MUST include ALL parts of the file, even if they haven't been modified. Do NOT include line numbers in the content.
-
-Usage:
-
-File path here
-
-Your file content here
-
-
-
-Example: Writing a configuration file
-
-frontend-config.json
-
-{
- "apiEndpoint": "https://api.example.com",
- "theme": {
- "primaryColor": "#007bff",
- "secondaryColor": "#6c757d",
- "fontFamily": "Arial, sans-serif"
- },
- "features": {
- "darkMode": true,
- "notifications": true,
- "analytics": false
- },
- "version": "1.0.0"
-}
-
-
-
-## ask_followup_question
-Description: Ask the user a question to gather additional information needed to complete the task. Use when you need clarification or more details to proceed effectively.
-
-Parameters:
-- question: (required) A clear, specific question addressing the information needed
-- follow_up: (required) A list of 2-4 suggested answers, each in its own tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
-
-Usage:
-
-Your question here
-
-First suggestion
-Action with mode switch
-
-
-
-Example:
-
-What is the path to the frontend-config.json file?
-
-./src/frontend-config.json
-./config/frontend-config.json
-./frontend-config.json
-
-
-
-## attempt_completion
-Description: After each tool use, the user will respond with the result of that tool use, i.e. if it succeeded or failed, along with any reasons for failure. Once you've received the results of tool uses and can confirm that the task is complete, use this tool to present the result of your work to the user. The user may respond with feedback if they are not satisfied with the result, which you can use to make improvements and try again.
-IMPORTANT NOTE: This tool CANNOT be used until you've confirmed from the user that any previous tool uses were successful. Failure to do so will result in code corruption and system failure. Before using this tool, you must confirm that you've received successful results from the user for any previous tool uses. If not, then DO NOT use this tool.
-Parameters:
-- result: (required) The result of the task. Formulate this result in a way that is final and does not require further input from the user. Don't end your result with questions or offers for further assistance.
-Usage:
-
-
-Your final result description here
-
-
-
-Example: Requesting to attempt completion with a result
-
-
-I've updated the CSS
-
-
-
-## switch_mode
-Description: Request to switch to a different mode. This tool allows modes to request switching to another mode when needed, such as switching to Code mode to make code changes. The user must approve the mode switch.
-Parameters:
-- mode_slug: (required) The slug of the mode to switch to (e.g., "code", "ask", "architect")
-- reason: (optional) The reason for switching modes
-Usage:
-
-Mode slug here
-Reason for switching here
-
-
-Example: Requesting to switch to code mode
-
-code
-Need to make code changes
-
-
-## new_task
-Description: This will let you create a new task instance in the chosen mode using your provided message.
-
-Parameters:
-- mode: (required) The slug of the mode to start the new task in (e.g., "code", "debug", "architect").
-- message: (required) The initial user message or instructions for this new task.
-
-Usage:
-
-your-mode-slug-here
-Your initial instructions here
-
-
-Example:
-
-code
-Implement a new feature for the application
-
-
-
-## update_todo_list
-
-**Description:**
-Replace the entire TODO list with an updated checklist reflecting the current state. Always provide the full list; the system will overwrite the previous one. This tool is designed for step-by-step task tracking, allowing you to confirm completion of each step before updating, update multiple task statuses at once (e.g., mark one as completed and start the next), and dynamically add new todos discovered during long or complex tasks.
-
-**Checklist Format:**
-- Use a single-level markdown checklist (no nesting or subtasks).
-- List todos in the intended execution order.
-- Status options:
- - [ ] Task description (pending)
- - [x] Task description (completed)
- - [-] Task description (in progress)
-
-**Status Rules:**
-- [ ] = pending (not started)
-- [x] = completed (fully finished, no unresolved issues)
-- [-] = in_progress (currently being worked on)
-
-**Core Principles:**
-- Before updating, always confirm which todos have been completed since the last update.
-- You may update multiple statuses in a single update (e.g., mark the previous as completed and the next as in progress).
-- When a new actionable item is discovered during a long or complex task, add it to the todo list immediately.
-- Do not remove any unfinished todos unless explicitly instructed.
-- Always retain all unfinished tasks, updating their status as needed.
-- Only mark a task as completed when it is fully accomplished (no partials, no unresolved dependencies).
-- If a task is blocked, keep it as in_progress and add a new todo describing what needs to be resolved.
-- Remove tasks only if they are no longer relevant or if the user requests deletion.
-
-**Usage Example:**
-
-
-[x] Analyze requirements
-[x] Design architecture
-[-] Implement core logic
-[ ] Write tests
-[ ] Update documentation
-
-
-
-*After completing "Implement core logic" and starting "Write tests":*
-
-
-[x] Analyze requirements
-[x] Design architecture
-[x] Implement core logic
-[-] Write tests
-[ ] Update documentation
-[ ] Add performance benchmarks
-
-
-
-**When to Use:**
-- The task is complicated or involves multiple steps or requires ongoing tracking.
-- You need to update the status of several todos at once.
-- New actionable items are discovered during task execution.
-- The user requests a todo list or provides multiple tasks.
-- The task is complex and benefits from clear, stepwise progress tracking.
-
-**When NOT to Use:**
-- There is only a single, trivial task.
-- The task can be completed in one or two simple steps.
-- The request is purely conversational or informational.
-
-**Task Management Guidelines:**
-- Mark task as completed immediately after all work of the current task is done.
-- Start the next task by marking it as in_progress.
-- Add new todos as soon as they are identified.
-- Use clear, descriptive task names.
-
-
-# Tool Use Guidelines
+ # Tool Use Guidelines
1. Assess what information you already have and what information you need to proceed with the task.
2. Choose the most appropriate tool based on the task and the tool descriptions provided. Assess if you need additional information to proceed, and which of the available tools would be most effective for gathering this information. For example using the list_files tool is more effective than running a command like `ls` in the terminal. It's critical that you think about each available tool and use the one that best fits the current step in the task.
-3. If multiple actions are needed, use one tool at a time per message to accomplish the task iteratively, with each tool use being informed by the result of the previous tool use. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
-4. Formulate your tool use using the XML format specified for each tool.
-5. After each tool use, the user will respond with the result of that tool use. This result will provide you with the necessary information to continue your task or make further decisions. This response may include:
- - Information about whether the tool succeeded or failed, along with any reasons for failure.
- - Linter errors that may have arisen due to the changes you made, which you'll need to address.
- - New terminal output in reaction to the changes, which you may need to consider or act upon.
- - Any other relevant feedback or information related to the tool use.
-6. ALWAYS wait for user confirmation after each tool use before proceeding. Never assume the success of a tool use without explicit confirmation of the result from the user.
-
-It is crucial to proceed step-by-step, waiting for the user's message after each tool use before moving forward with the task. This approach allows you to:
-1. Confirm the success of each step before proceeding.
-2. Address any issues or errors that arise immediately.
-3. Adapt your approach based on new information or unexpected results.
-4. Ensure that each action builds correctly on the previous ones.
-
-By waiting for and carefully considering the user's response after each tool use, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
-
+3. If multiple actions are needed, you may use multiple tools in a single message when appropriate, or use tools iteratively across messages. Each tool use should be informed by the results of previous tool uses. Do not assume the outcome of any tool use. Each step must be informed by the previous step's result.
+By carefully considering the user's response after tool executions, you can react accordingly and make informed decisions about how to proceed with the task. This iterative process helps ensure the overall success and accuracy of your work.
====
@@ -385,7 +39,7 @@ MODES
RULES
- The project base directory is: /test/path
-- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to .
+- All file paths must be relative to this directory. However, commands may change directories in terminals, so respect working directory specified by the response to execute_command.
- You cannot `cd` into a different directory to complete a task. You are stuck operating from '/test/path', so be sure to pass in the correct 'path' parameter when using tools that require a path.
- Do not use the ~ character or $HOME to refer to the home directory.
- Before using the execute_command tool, you must first think about the SYSTEM INFORMATION context provided to understand the user's environment and tailor your commands to ensure they are compatible with their system. You must also consider if the command you need to run should be executed in a specific directory outside of the current working directory '/test/path', and if so prepend with `cd`'ing into that directory && then executing the command (as one command since you are stuck operating from '/test/path'). For example, if you needed to run `npm install` in a project outside of '/test/path', you would need to prepend with a `cd` i.e. pseudocode for this would be `cd (path to project) && (command, in this case npm install)`.
@@ -434,7 +88,7 @@ You accomplish a given task iteratively, breaking it down into clear steps and w
USER'S CUSTOM INSTRUCTIONS
-The following additional instructions are provided by the user, and should be followed to the best of your ability without interfering with the TOOL USE guidelines.
+The following additional instructions are provided by the user, and should be followed to the best of your ability.
Language Preference:
You should always speak and think in the "en" language.
diff --git a/src/core/prompts/__tests__/add-custom-instructions.spec.ts b/src/core/prompts/__tests__/add-custom-instructions.spec.ts
index daf4d961f1..b7813d0f5b 100644
--- a/src/core/prompts/__tests__/add-custom-instructions.spec.ts
+++ b/src/core/prompts/__tests__/add-custom-instructions.spec.ts
@@ -210,9 +210,7 @@ describe("addCustomInstructions", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -233,9 +231,7 @@ describe("addCustomInstructions", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -244,32 +240,6 @@ describe("addCustomInstructions", () => {
expect(prompt).toMatchFileSnapshot("./__snapshots__/add-custom-instructions/ask-mode-prompt.snap")
})
- it("should include MCP server creation info when enabled", async () => {
- const mockMcpHub = createMockMcpHub(true)
-
- const prompt = await SYSTEM_PROMPT(
- mockContext,
- "/test/path",
- false, // supportsImages
- mockMcpHub, // mcpHub
- undefined, // diffStrategy
- undefined, // browserViewportSize
- defaultModeSlug, // mode
- undefined, // customModePrompts
- undefined, // customModes,
- undefined, // globalCustomInstructions
- undefined, // diffEnabled
- undefined, // experiments
- true, // enableMcpServerCreation
- undefined, // language
- undefined, // rooIgnoreInstructions
- undefined, // partialReadsEnabled
- )
-
- expect(prompt).toContain("Creating an MCP Server")
- expect(prompt).toMatchFileSnapshot("./__snapshots__/add-custom-instructions/mcp-server-creation-enabled.snap")
- })
-
it("should exclude MCP server creation info when disabled", async () => {
const mockMcpHub = createMockMcpHub(false)
@@ -284,9 +254,7 @@ describe("addCustomInstructions", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
- false, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -308,9 +276,7 @@ describe("addCustomInstructions", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
true, // partialReadsEnabled
diff --git a/src/core/prompts/__tests__/custom-system-prompt.spec.ts b/src/core/prompts/__tests__/custom-system-prompt.spec.ts
index 6106e16174..0ec2956b31 100644
--- a/src/core/prompts/__tests__/custom-system-prompt.spec.ts
+++ b/src/core/prompts/__tests__/custom-system-prompt.spec.ts
@@ -104,9 +104,7 @@ describe("File-Based Custom System Prompt", () => {
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -142,9 +140,7 @@ describe("File-Based Custom System Prompt", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -188,9 +184,7 @@ describe("File-Based Custom System Prompt", () => {
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
diff --git a/src/core/prompts/__tests__/responses-rooignore.spec.ts b/src/core/prompts/__tests__/responses-rooignore.spec.ts
index ca0dcfbad5..03aae96776 100644
--- a/src/core/prompts/__tests__/responses-rooignore.spec.ts
+++ b/src/core/prompts/__tests__/responses-rooignore.spec.ts
@@ -51,10 +51,13 @@ describe("RooIgnore Response Formatting", () => {
it("should format error message for ignored files", () => {
const errorMessage = formatResponse.rooIgnoreError("secrets/api-keys.json")
- // Verify error message format
- expect(errorMessage).toContain("Access to secrets/api-keys.json is blocked by the .rooignore file settings")
- expect(errorMessage).toContain("continue in the task without using this file")
- expect(errorMessage).toContain("ask the user to update the .rooignore file")
+ // Verify error message format (JSON)
+ const parsed = JSON.parse(errorMessage) as any
+ expect(parsed.status).toBe("error")
+ expect(parsed.type).toBe("access_denied")
+ expect(parsed.path).toBe("secrets/api-keys.json")
+ expect(parsed.suggestion).toContain("continue without this file")
+ expect(parsed.suggestion).toContain("update the .rooignore file")
})
/**
@@ -66,7 +69,8 @@ describe("RooIgnore Response Formatting", () => {
// Test each path
for (const testPath of paths) {
const errorMessage = formatResponse.rooIgnoreError(testPath)
- expect(errorMessage).toContain(`Access to ${testPath} is blocked`)
+ const parsed = JSON.parse(errorMessage) as any
+ expect(parsed.path).toBe(testPath)
}
})
})
diff --git a/src/core/prompts/__tests__/system-prompt.spec.ts b/src/core/prompts/__tests__/system-prompt.spec.ts
index a0953b2de1..91fb9350b4 100644
--- a/src/core/prompts/__tests__/system-prompt.spec.ts
+++ b/src/core/prompts/__tests__/system-prompt.spec.ts
@@ -112,9 +112,7 @@ __setMockImplementation(
}
const joinedSections = sections.join("\n\n")
- const effectiveProtocol = options?.settings?.toolProtocol || "xml"
- const skipXmlReferences = effectiveProtocol === "native"
- const toolUseRef = skipXmlReferences ? "." : " without interfering with the TOOL USE guidelines."
+ const toolUseRef = "."
return joinedSections
? `\n====\n\nUSER'S CUSTOM INSTRUCTIONS\n\nThe following additional instructions are provided by the user, and should be followed to the best of your ability${toolUseRef}\n\n${joinedSections}`
: ""
@@ -227,9 +225,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -250,9 +246,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -275,9 +269,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -298,9 +290,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -321,9 +311,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -332,78 +320,6 @@ describe("SYSTEM_PROMPT", () => {
expect(prompt).toMatchFileSnapshot("./__snapshots__/system-prompt/with-different-viewport-size.snap")
})
- it("should include diff strategy tool description when diffEnabled is true", async () => {
- const prompt = await SYSTEM_PROMPT(
- mockContext,
- "/test/path",
- false,
- undefined, // mcpHub
- new MultiSearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase
- undefined, // browserViewportSize
- defaultModeSlug, // mode
- undefined, // customModePrompts
- undefined, // customModes
- undefined, // globalCustomInstructions
- true, // diffEnabled
- experiments,
- true, // enableMcpServerCreation
- undefined, // language
- undefined, // rooIgnoreInstructions
- undefined, // partialReadsEnabled
- )
-
- expect(prompt).toContain("apply_diff")
- expect(prompt).toMatchFileSnapshot("./__snapshots__/system-prompt/with-diff-enabled-true.snap")
- })
-
- it("should exclude diff strategy tool description when diffEnabled is false", async () => {
- const prompt = await SYSTEM_PROMPT(
- mockContext,
- "/test/path",
- false, // supportsImages
- undefined, // mcpHub
- new MultiSearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase
- undefined, // browserViewportSize
- defaultModeSlug, // mode
- undefined, // customModePrompts
- undefined, // customModes
- undefined, // globalCustomInstructions
- false, // diffEnabled
- experiments,
- true, // enableMcpServerCreation
- undefined, // language
- undefined, // rooIgnoreInstructions
- undefined, // partialReadsEnabled
- )
-
- expect(prompt).not.toContain("apply_diff")
- expect(prompt).toMatchFileSnapshot("./__snapshots__/system-prompt/with-diff-enabled-false.snap")
- })
-
- it("should exclude diff strategy tool description when diffEnabled is undefined", async () => {
- const prompt = await SYSTEM_PROMPT(
- mockContext,
- "/test/path",
- false,
- undefined, // mcpHub
- new MultiSearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase
- undefined, // browserViewportSize
- defaultModeSlug, // mode
- undefined, // customModePrompts
- undefined, // customModes
- undefined, // globalCustomInstructions
- undefined, // diffEnabled
- experiments,
- true, // enableMcpServerCreation
- undefined, // language
- undefined, // rooIgnoreInstructions
- undefined, // partialReadsEnabled
- )
-
- expect(prompt).not.toContain("apply_diff")
- expect(prompt).toMatchFileSnapshot("./__snapshots__/system-prompt/with-diff-enabled-undefined.snap")
- })
-
it("should include vscode language in custom instructions", async () => {
// Mock vscode.env.language
const vscode = vi.mocked(await import("vscode")) as any
@@ -443,9 +359,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -504,9 +418,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
customModes, // customModes
"Global instructions", // globalCustomInstructions
- undefined, // diffEnabled
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -542,9 +454,7 @@ describe("SYSTEM_PROMPT", () => {
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
- false, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -575,9 +485,7 @@ describe("SYSTEM_PROMPT", () => {
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
- false, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -593,7 +501,6 @@ describe("SYSTEM_PROMPT", () => {
todoListEnabled: false,
useAgentRules: true,
newTaskRequireTodos: false,
- toolProtocol: "xml" as const,
}
const prompt = await SYSTEM_PROMPT(
@@ -607,9 +514,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -627,7 +532,6 @@ describe("SYSTEM_PROMPT", () => {
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: false,
- toolProtocol: "xml" as const,
}
const prompt = await SYSTEM_PROMPT(
@@ -641,17 +545,16 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
settings, // settings
)
+ // update_todo_list is still referenced by mode instructions, but tool catalogs are not embedded.
expect(prompt).toContain("update_todo_list")
- expect(prompt).toContain("## update_todo_list")
+ expect(prompt).not.toContain("## update_todo_list")
})
it("should include update_todo_list tool when todoListEnabled is undefined", async () => {
@@ -660,7 +563,6 @@ describe("SYSTEM_PROMPT", () => {
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: false,
- toolProtocol: "xml" as const,
}
const prompt = await SYSTEM_PROMPT(
@@ -674,26 +576,24 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
settings, // settings
)
+ // update_todo_list is still referenced by mode instructions, but tool catalogs are not embedded.
expect(prompt).toContain("update_todo_list")
- expect(prompt).toContain("## update_todo_list")
+ expect(prompt).not.toContain("## update_todo_list")
})
- it("should include XML tool instructions when disableXmlToolInstructions is false (default)", async () => {
+ it("should include native tool instructions", async () => {
const settings = {
maxConcurrentFileReads: 5,
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: false,
- toolProtocol: "xml" as const, // explicitly xml
}
const prompt = await SYSTEM_PROMPT(
@@ -707,81 +607,7 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
- true, // enableMcpServerCreation
- undefined, // language
- undefined, // rooIgnoreInstructions
- undefined, // partialReadsEnabled
- settings, // settings
- )
-
- // Should contain XML guidance sections
- expect(prompt).toContain("TOOL USE")
- expect(prompt).toContain("XML-style tags")
- expect(prompt).toContain("")
- expect(prompt).toContain("")
- expect(prompt).toContain("Tool Use Guidelines")
- expect(prompt).toContain("# Tools")
-
- // Should contain tool descriptions with XML examples
- expect(prompt).toContain("## read_file")
- expect(prompt).toContain("")
- expect(prompt).toContain("")
-
- // Should be byte-for-byte compatible with default behavior
- const defaultPrompt = await SYSTEM_PROMPT(
- mockContext,
- "/test/path",
- false,
- undefined,
- undefined,
- undefined,
- defaultModeSlug,
- undefined,
- undefined,
- undefined,
- undefined,
- experiments,
- true,
- undefined,
- undefined,
- undefined,
- {
- maxConcurrentFileReads: 5,
- todoListEnabled: true,
- useAgentRules: true,
- newTaskRequireTodos: false,
- toolProtocol: "xml" as const,
- },
- )
-
- expect(prompt).toBe(defaultPrompt)
- })
-
- it("should include native tool instructions when toolProtocol is native", async () => {
- const settings = {
- maxConcurrentFileReads: 5,
- todoListEnabled: true,
- useAgentRules: true,
- newTaskRequireTodos: false,
- toolProtocol: "native" as const, // native protocol
- }
-
- const prompt = await SYSTEM_PROMPT(
- mockContext,
- "/test/path",
- false,
- undefined, // mcpHub
- undefined, // diffStrategy
- undefined, // browserViewportSize
- defaultModeSlug, // mode
- undefined, // customModePrompts
- undefined, // customModes
- undefined, // globalCustomInstructions
- undefined, // diffEnabled
- experiments,
- true, // enableMcpServerCreation
undefined, // language
undefined, // rooIgnoreInstructions
undefined, // partialReadsEnabled
@@ -794,17 +620,13 @@ describe("SYSTEM_PROMPT", () => {
expect(prompt).toContain("Do not include XML markup or examples")
// Should NOT contain XML-style tags or examples
- expect(prompt).not.toContain("XML-style tags")
expect(prompt).not.toContain("")
expect(prompt).not.toContain("")
- // Should contain Tool Use Guidelines section without format-specific guidance
+ // Should contain Tool Use Guidelines section
expect(prompt).toContain("Tool Use Guidelines")
- // Should NOT contain any protocol-specific formatting instructions
- expect(prompt).not.toContain("provider's native tool-calling mechanism")
- expect(prompt).not.toContain("XML format specified for each tool")
- // Should NOT contain # Tools catalog at all in native mode
+ // Should NOT contain a tool catalog / XML examples
expect(prompt).not.toContain("# Tools")
expect(prompt).not.toContain("## read_file")
expect(prompt).not.toContain("## execute_command")
@@ -821,43 +643,6 @@ describe("SYSTEM_PROMPT", () => {
expect(prompt).toContain("OBJECTIVE")
})
- it("should default to XML tool instructions when toolProtocol is undefined", async () => {
- const settings = {
- maxConcurrentFileReads: 5,
- todoListEnabled: true,
- useAgentRules: true,
- newTaskRequireTodos: false,
- toolProtocol: "xml" as const,
- }
-
- const prompt = await SYSTEM_PROMPT(
- mockContext,
- "/test/path",
- false,
- undefined, // mcpHub
- undefined, // diffStrategy
- undefined, // browserViewportSize
- defaultModeSlug, // mode
- undefined, // customModePrompts
- undefined, // customModes
- undefined, // globalCustomInstructions
- undefined, // diffEnabled
- experiments,
- true, // enableMcpServerCreation
- undefined, // language
- undefined, // rooIgnoreInstructions
- undefined, // partialReadsEnabled
- settings, // settings
- )
-
- // Should contain XML guidance (default behavior)
- expect(prompt).toContain("TOOL USE")
- expect(prompt).toContain("XML-style tags")
- expect(prompt).toContain("")
- expect(prompt).toContain("Tool Use Guidelines")
- expect(prompt).toContain("# Tools")
- })
-
afterAll(() => {
vi.restoreAllMocks()
})
diff --git a/src/core/prompts/instructions/create-mode.ts b/src/core/prompts/instructions/create-mode.ts
deleted file mode 100644
index 80f69b0802..0000000000
--- a/src/core/prompts/instructions/create-mode.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-import * as path from "path"
-import * as vscode from "vscode"
-
-import { GlobalFileNames } from "../../../shared/globalFileNames"
-import { getSettingsDirectoryPath } from "../../../utils/storage"
-
-export async function createModeInstructions(context: vscode.ExtensionContext | undefined): Promise {
- if (!context) throw new Error("Missing VSCode Extension Context")
-
- const settingsDir = await getSettingsDirectoryPath(context.globalStorageUri.fsPath)
- const customModesPath = path.join(settingsDir, GlobalFileNames.customModes)
-
- return `
-Custom modes can be configured in two ways:
- 1. Globally via '${customModesPath}' (created automatically on startup)
- 2. Per-workspace via '.roomodes' in the workspace root directory
-
-When modes with the same slug exist in both files, the workspace-specific .roomodes version takes precedence. This allows projects to override global modes or define project-specific modes.
-
-
-If asked to create a project mode, create it in .roomodes in the workspace root. If asked to create a global mode, use the global custom modes file.
-
-- The following fields are required and must not be empty:
- * slug: A valid slug (lowercase letters, numbers, and hyphens). Must be unique, and shorter is better.
- * name: The display name for the mode
- * roleDefinition: A detailed description of the mode's role and capabilities
- * groups: Array of allowed tool groups (can be empty). Each group can be specified either as a string (e.g., "edit" to allow editing any file) or with file restrictions (e.g., ["edit", { fileRegex: "\\.md$", description: "Markdown files only" }] to only allow editing markdown files)
-
-- The following fields are optional but highly recommended:
- * description: A short, human-readable description of what this mode does (5 words)
- * whenToUse: A clear description of when this mode should be selected and what types of tasks it's best suited for. This helps the Orchestrator mode make better decisions.
- * customInstructions: Additional instructions for how the mode should operate
-
-- For multi-line text, include newline characters in the string like "This is the first line.\\nThis is the next line.\\n\\nThis is a double line break."
-
-Both files should follow this structure (in YAML format):
-
-customModes:
- - slug: designer # Required: unique slug with lowercase letters, numbers, and hyphens
- name: Designer # Required: mode display name
- description: UI/UX design systems expert # Optional but recommended: short description (5 words)
- roleDefinition: >-
- You are Roo, a UI/UX expert specializing in design systems and frontend development. Your expertise includes:
- - Creating and maintaining design systems
- - Implementing responsive and accessible web interfaces
- - Working with CSS, HTML, and modern frontend frameworks
- - Ensuring consistent user experiences across platforms # Required: non-empty
- whenToUse: >-
- Use this mode when creating or modifying UI components, implementing design systems,
- or ensuring responsive web interfaces. This mode is especially effective with CSS,
- HTML, and modern frontend frameworks. # Optional but recommended
- groups: # Required: array of tool groups (can be empty)
- - read # Read files group (read_file, fetch_instructions, search_files, list_files)
- - edit # Edit files group (apply_diff, write_to_file) - allows editing any file
- # Or with file restrictions:
- # - - edit
- # - fileRegex: \\.md$
- # description: Markdown files only # Edit group that only allows editing markdown files
- - browser # Browser group (browser_action)
- - command # Command group (execute_command)
- - mcp # MCP group (use_mcp_tool, access_mcp_resource)
- customInstructions: Additional instructions for the Designer mode # Optional`
-}
diff --git a/src/core/prompts/instructions/instructions.ts b/src/core/prompts/instructions/instructions.ts
deleted file mode 100644
index c1ff2a1899..0000000000
--- a/src/core/prompts/instructions/instructions.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { createMCPServerInstructions } from "./create-mcp-server"
-import { createModeInstructions } from "./create-mode"
-import { McpHub } from "../../../services/mcp/McpHub"
-import { DiffStrategy } from "../../../shared/tools"
-import * as vscode from "vscode"
-
-interface InstructionsDetail {
- mcpHub?: McpHub
- diffStrategy?: DiffStrategy
- context?: vscode.ExtensionContext
-}
-
-export async function fetchInstructions(text: string, detail: InstructionsDetail): Promise {
- switch (text) {
- case "create_mcp_server": {
- return await createMCPServerInstructions(detail.mcpHub, detail.diffStrategy)
- }
- case "create_mode": {
- return await createModeInstructions(detail.context)
- }
- default: {
- return ""
- }
- }
-}
diff --git a/src/core/prompts/responses.ts b/src/core/prompts/responses.ts
index ccb09e68e1..60b5b4123a 100644
--- a/src/core/prompts/responses.ts
+++ b/src/core/prompts/responses.ts
@@ -3,67 +3,44 @@ import * as path from "path"
import * as diff from "diff"
import { RooIgnoreController, LOCK_TEXT_SYMBOL } from "../ignore/RooIgnoreController"
import { RooProtectedController } from "../protect/RooProtectedController"
-import { ToolProtocol, isNativeProtocol, TOOL_PROTOCOL } from "@roo-code/types"
export const formatResponse = {
- toolDenied: (protocol?: ToolProtocol) => {
- if (isNativeProtocol(protocol ?? TOOL_PROTOCOL.XML)) {
- return JSON.stringify({
- status: "denied",
- message: "The user denied this operation.",
- })
- }
- return `The user denied this operation.`
- },
+ toolDenied: () =>
+ JSON.stringify({
+ status: "denied",
+ message: "The user denied this operation.",
+ }),
- toolDeniedWithFeedback: (feedback?: string, protocol?: ToolProtocol) => {
- if (isNativeProtocol(protocol ?? TOOL_PROTOCOL.XML)) {
- return JSON.stringify({
- status: "denied",
- message: "The user denied this operation and provided the following feedback",
- feedback: feedback,
- })
- }
- return `The user denied this operation and provided the following feedback:\n