>
)}
diff --git a/apps/web-roo-code/src/app/cloud/page.tsx b/apps/web-roo-code/src/app/cloud/page.tsx
index ba2edc83d4..1da9cad2af 100644
--- a/apps/web-roo-code/src/app/cloud/page.tsx
+++ b/apps/web-roo-code/src/app/cloud/page.tsx
@@ -98,8 +98,7 @@ const features: Feature[] = [
{
icon: Brain,
title: "Model Agnostic",
- description:
- "Bring your own keys or use the Roo Code Router with access to all top models with no markup.",
+ description: "Bring your own keys or use the Roo Code Router with access to all top models with no markup.",
},
{
icon: Github,
@@ -115,8 +114,7 @@ const features: Feature[] = [
{
icon: Router,
title: "Roomote Control",
- description:
- "Connect to your local VS Code instance and control the extension remotely from the browser.",
+ description: "Connect to your local VS Code instance and control the extension remotely from the browser.",
},
{
icon: Users,
@@ -153,7 +151,7 @@ export default function CloudPage() {
Your AI Team in the Cloud
- Create your agent team in the Cloud, give them access to GitHub, and start delegating tasks
+ Create your agent team in the Cloud, give them access to GitHub, and start delegating tasks
from the web, Slack, Linear, and more.
diff --git a/apps/web-roo-code/src/app/legal/cookies/page.tsx b/apps/web-roo-code/src/app/legal/cookies/page.tsx
index c8058a34e7..895d7c2b45 100644
--- a/apps/web-roo-code/src/app/legal/cookies/page.tsx
+++ b/apps/web-roo-code/src/app/legal/cookies/page.tsx
@@ -100,6 +100,19 @@ export default function CookiePolicy() {
@@ -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 6ae6e9993b..6851b47b6a 100644
--- a/apps/web-roo-code/src/app/pricing/page.tsx
+++ b/apps/web-roo-code/src/app/pricing/page.tsx
@@ -239,8 +239,8 @@ export default function PricingPage() {
On any plan, you can use your own LLM provider API key or use the built-in Roo Code
- Router – curated models to work with Roo with no markup, including the
- latest Gemini, GPT and Claude. Paid with credits.
+ Router – curated models to work with Roo with no markup, including the latest
+ Gemini, GPT and Claude. Paid with credits.
See per model pricing.
@@ -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..9adac74fd2 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,7 +264,7 @@ 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 () => {
+ it("should send tool_result with is_error for skipped tools in native tool calling when didAlreadyUseTool is true", async () => {
// Simulate multiple tool calls with native protocol
const toolCallId1 = "tool_call_003"
const toolCallId2 = "tool_call_004"
@@ -306,18 +309,15 @@ describe("presentAssistantMessage - Image Handling in Native Tool Calls", () =>
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)
+ it("should reject subsequent tool calls when a legacy/XML-style tool call is encountered", async () => {
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 +330,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..ed90127b5b 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,
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..e3f652d352 100644
--- a/src/core/assistant-message/presentAssistantMessage.ts
+++ b/src/core/assistant-message/presentAssistantMessage.ts
@@ -10,7 +10,6 @@ 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"
@@ -18,9 +17,7 @@ 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 { writeToFileTool } from "../tools/WriteToFileTool"
-import { applyDiffTool } from "../tools/MultiApplyDiffTool"
import { searchAndReplaceTool } from "../tools/SearchAndReplaceTool"
import { searchReplaceTool } from "../tools/SearchReplaceTool"
import { editFileTool } from "../tools/EditFileTool"
@@ -38,7 +35,7 @@ import { updateTodoListTool } from "../tools/UpdateTodoListTool"
import { runSlashCommandTool } from "../tools/RunSlashCommandTool"
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"
@@ -146,7 +143,6 @@ export async function presentAssistantMessage(cline: Task) {
// 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 +170,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
@@ -219,14 +215,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 +245,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 +288,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag: (tag, text) => text || "",
- toolProtocol,
})
break
}
@@ -313,58 +302,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 +323,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 ?? {}
@@ -392,24 +367,8 @@ export async function presentAssistantMessage(cline: 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}'` : ""
@@ -457,185 +416,121 @@ 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
+ // For native tool calling, we must send a tool_result for every tool_use to avoid API errors
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,
- })
- }
+ cline.pushToolResultToUserContent({
+ type: "tool_result",
+ tool_use_id: toolCallId,
+ content: errorMessage,
+ is_error: true,
+ })
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
+ cline.didAlreadyUseTool = true
}
const askApproval = async (
@@ -656,14 +551,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 +592,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 +628,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 +648,7 @@ export async function presentAssistantMessage(cline: Task) {
block.name as ToolName,
mode ?? defaultModeSlug,
customModes ?? [],
- { apply_diff: cline.diffEnabled },
+ {},
block.params,
stateExperiments,
includedTools,
@@ -793,24 +656,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 +719,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 +732,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "update_todo_list":
@@ -885,59 +739,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 +763,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "edit_file":
@@ -956,8 +771,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "apply_patch":
@@ -966,8 +779,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "read_file":
@@ -976,8 +787,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "fetch_instructions":
@@ -985,8 +794,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "list_files":
@@ -994,8 +801,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "codebase_search":
@@ -1003,8 +808,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "search_files":
@@ -1012,8 +815,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "browser_action":
@@ -1023,7 +824,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
)
break
case "execute_command":
@@ -1031,8 +831,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "use_mcp_tool":
@@ -1040,8 +838,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "access_mcp_resource":
@@ -1049,8 +845,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "ask_followup_question":
@@ -1058,8 +852,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "switch_mode":
@@ -1067,8 +859,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "new_task":
@@ -1076,8 +866,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
toolCallId: block.id,
})
break
@@ -1086,10 +874,8 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
askFinishSubTaskApproval,
toolDescription,
- toolProtocol,
}
await attemptCompletionTool.handle(
cline,
@@ -1103,8 +889,6 @@ export async function presentAssistantMessage(cline: Task) {
askApproval,
handleError,
pushToolResult,
- removeClosingTag,
- toolProtocol,
})
break
case "generate_image":
@@ -1113,13 +897,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 +924,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 +955,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 +1042,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",
+ "fetch_instructions",
+ "generate_image",
+ "list_files",
+ "new_task",
+ "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/condense/__tests__/condense.spec.ts b/src/core/condense/__tests__/condense.spec.ts
index bea7d50ac1..dcb05cd74d 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,77 @@ describe("Condense", () => {
{ role: "user", content: "Ninth message" },
]
- const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
+ const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 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, mockApiHandler, "System prompt", taskId, 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, mockApiHandler, "System prompt", taskId, 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 +227,35 @@ describe("Condense", () => {
{ role: "user", content: "Perfect!" },
]
- const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
+ const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 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, mockApiHandler, "System prompt", taskId, false)
- // Should return an error since we have only 4 messages (first + 3 to keep)
+ // Should return an error since we have only 1 message
expect(result.error).toBeDefined()
expect(result.messages).toEqual(messages) // Original messages unchanged
expect(result.summary).toBe("")
})
- it("should not summarize messages that already contain a recent summary", async () => {
+ it("should not summarize messages that already contain a recent summary with no new messages", async () => {
const messages: ApiMessage[] = [
{ role: "user", content: "First message with /command" },
- { role: "assistant", content: "Old message" },
- { role: "user", content: "Message before summary" },
- { role: "assistant", content: "Response" },
- { role: "user", content: "Another message" },
- { role: "assistant", content: "Previous summary", isSummary: true }, // Summary in last N messages
- { role: "user", content: "Final message" },
+ { role: "assistant", content: "Previous summary", isSummary: true },
]
- const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 5000, false)
+ const result = await summarizeConversation(messages, mockApiHandler, "System prompt", taskId, 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 +286,7 @@ describe("Condense", () => {
{ role: "user", content: "Seventh" },
]
- const result = await summarizeConversation(messages, emptyHandler, "System prompt", taskId, 5000, false)
+ const result = await summarizeConversation(messages, emptyHandler, "System prompt", taskId, false)
expect(result.error).toBeDefined()
expect(result.messages).toEqual(messages)
@@ -225,6 +294,81 @@ describe("Condense", () => {
})
})
+ describe("getEffectiveApiHistory", () => {
+ it("should return only summary when summary exists (fresh start)", () => {
+ const condenseId = "test-condense-id"
+ const messages: ApiMessage[] = [
+ { role: "user", content: "First", condenseParent: condenseId },
+ { role: "assistant", content: "Second", condenseParent: condenseId },
+ { role: "user", content: "Third", condenseParent: condenseId },
+ {
+ role: "user",
+ content: [{ type: "text", text: "Summary content" }],
+ isSummary: true,
+ condenseId,
+ },
+ ]
+
+ const result = getEffectiveApiHistory(messages)
+
+ expect(result).toHaveLength(1)
+ expect(result[0].isSummary).toBe(true)
+ })
+
+ it("should include messages after summary in fresh start model", () => {
+ const condenseId = "test-condense-id"
+ const messages: ApiMessage[] = [
+ { role: "user", content: "First", condenseParent: condenseId },
+ { role: "assistant", content: "Second", condenseParent: condenseId },
+ {
+ role: "user",
+ content: [{ type: "text", text: "Summary content" }],
+ isSummary: true,
+ condenseId,
+ },
+ { role: "assistant", content: "New response after summary" },
+ { role: "user", content: "New user message" },
+ ]
+
+ const result = getEffectiveApiHistory(messages)
+
+ expect(result).toHaveLength(3)
+ expect(result[0].isSummary).toBe(true)
+ expect(result[1].content).toBe("New response after summary")
+ expect(result[2].content).toBe("New user message")
+ })
+
+ it("should return all messages when no summary exists", () => {
+ const messages: ApiMessage[] = [
+ { role: "user", content: "First" },
+ { role: "assistant", content: "Second" },
+ { role: "user", content: "Third" },
+ ]
+
+ const result = getEffectiveApiHistory(messages)
+
+ expect(result).toEqual(messages)
+ })
+
+ it("should restore messages when summary is deleted (rewind)", () => {
+ // After rewind, summary is deleted but condenseParent tags remain as orphans
+ // The cleanupAfterTruncation function would normally clear these,
+ // but even without cleanup, getEffectiveApiHistory should handle orphaned tags
+ const orphanedCondenseId = "deleted-summary-id"
+ const messages: ApiMessage[] = [
+ { role: "user", content: "First", condenseParent: orphanedCondenseId },
+ { role: "assistant", content: "Second", condenseParent: orphanedCondenseId },
+ { role: "user", content: "Third", condenseParent: orphanedCondenseId },
+ // Summary was deleted - no isSummary message exists
+ ]
+
+ const result = getEffectiveApiHistory(messages)
+
+ // With no summary, all messages should be included (orphaned condenseParent is ignored)
+ expect(result).toHaveLength(3)
+ })
+ })
+
describe("getMessagesSinceLastSummary", () => {
it("should return all messages when no summary exists", () => {
const messages: ApiMessage[] = [
@@ -241,39 +385,48 @@ describe("Condense", () => {
const messages: ApiMessage[] = [
{ role: "user", content: "First message" },
{ role: "assistant", content: "Second message" },
- { role: "assistant", content: "Summary content", isSummary: true },
- { role: "user", content: "Message after summary" },
- { role: "assistant", content: "Final message" },
+ { role: "user", content: "Summary content", isSummary: true },
+ { role: "assistant", content: "Message after summary" },
+ { role: "user", content: "Final message" },
]
const result = getMessagesSinceLastSummary(messages)
- // Should include the original first user message for context preservation, the summary, and messages after
- expect(result[0].role).toBe("user")
- expect(result[0].content).toBe("First message") // Preserves original first message
- expect(result[1]).toEqual(messages[2]) // The summary
- expect(result[2]).toEqual(messages[3])
- expect(result[3]).toEqual(messages[4])
+ expect(result[0]).toEqual(messages[2]) // The summary
+ expect(result[1]).toEqual(messages[3])
+ expect(result[2]).toEqual(messages[4])
})
it("should handle multiple summaries and return from the last one", () => {
const messages: ApiMessage[] = [
{ role: "user", content: "First message" },
- { role: "assistant", content: "First summary", isSummary: true },
- { role: "user", content: "Middle message" },
- { role: "assistant", content: "Second summary", isSummary: true },
- { role: "user", content: "Recent message" },
- { role: "assistant", content: "Final message" },
+ { role: "user", content: "First summary", isSummary: true },
+ { role: "assistant", content: "Middle message" },
+ { role: "user", content: "Second summary", isSummary: true },
+ { role: "assistant", content: "Recent message" },
+ { role: "user", content: "Final message" },
]
const result = getMessagesSinceLastSummary(messages)
- // Should only include from the last summary with original first message preserved
- expect(result[0].role).toBe("user")
- expect(result[0].content).toBe("First message") // Preserves original first message
- expect(result[1]).toEqual(messages[3]) // Second summary
- expect(result[2]).toEqual(messages[4])
- expect(result[3]).toEqual(messages[5])
+ expect(result[0]).toEqual(messages[3]) // Second summary
+ expect(result[1]).toEqual(messages[4])
+ expect(result[2]).toEqual(messages[5])
+ })
+
+ it("should prepend first user message when summary starts with assistant", () => {
+ const messages: ApiMessage[] = [
+ { role: "user", content: "Original first message" },
+ { role: "assistant", content: "Summary content", isSummary: true },
+ { role: "user", content: "After summary" },
+ ]
+
+ const result = getMessagesSinceLastSummary(messages)
+
+ // Should prepend original first message for Bedrock compatibility
+ expect(result[0]).toEqual(messages[0]) // Original first user message
+ expect(result[1]).toEqual(messages[1]) // The summary
+ expect(result[2]).toEqual(messages[2])
})
})
})
diff --git a/src/core/condense/__tests__/index.spec.ts b/src/core/condense/__tests__/index.spec.ts
index 605dcd7fb5..8a0f3bea63 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,628 +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 find tool_use even when it is far back in the message history (ROO-520 fix)", () => {
- // This test verifies the fix for https://linear.app/roocode/issue/ROO-520
- // The bug occurred when tool_use was preserved in an earlier summary message
- // after multiple condensations. The bounded search window was too small to find it,
- // resulting in orphaned tool_result blocks that caused 400 errors from the API.
- const toolResultBlock = {
- type: "tool_result" as const,
- tool_use_id: "toolu_early_in_history",
- content: "result",
- }
-
- // Tool_use is at index 0, tool_result is in the last 3 messages.
- // The search must cover the ENTIRE condensed region to find the tool_use.
- const messages: ApiMessage[] = [
{
role: "assistant",
content: [
- { type: "text" as const, text: "Early tool call..." },
- {
- type: "tool_use" as const,
- id: "toolu_early_in_history",
- name: "read_file",
- input: { path: "test.txt" },
- },
+ { 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
]
- 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")
- // With the fix, tool_use should be found even though it's far back in history
- expect(result.toolUseBlocksToPreserve).toHaveLength(1)
- expect(result.toolUseBlocksToPreserve[0].id).toBe("toolu_early_in_history")
+ 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 find tool_use in previous summary message after multiple condensations (ROO-520)", () => {
- // This test simulates the exact scenario from ROO-520:
- // After first condensation, tool_use was preserved in summary1.
- // After second condensation, the bounded search couldn't find tool_use in summary1
- // because it was outside the N_MESSAGES_TO_KEEP search window.
- const toolUseBlock = {
- type: "tool_use" as const,
- id: "toolu_in_summary",
- name: "read_file",
- input: { path: "test.txt" },
- }
- const toolResultBlock = {
- type: "tool_result" as const,
- tool_use_id: "toolu_in_summary",
- content: "file contents",
- }
-
- // Simulate state after first condensation:
- // summary1 contains a preserved tool_use block from the first condense
- const summaryMessage: ApiMessage = {
- role: "assistant",
- content: [{ type: "text" as const, text: "Summary of first conversation chunk" }, toolUseBlock],
- ts: 5,
- isSummary: true,
- condenseId: "summary-1",
- }
-
+ it("should inject synthetic tool_results for multiple orphan tool_calls", () => {
const messages: ApiMessage[] = [
- { role: "user", content: "First message", ts: 1 },
- // Messages 2-4 are tagged with condenseParent from first condensation
- { role: "assistant", content: "Message 2", ts: 2, condenseParent: "summary-1" },
- { role: "user", content: "Message 3", ts: 3, condenseParent: "summary-1" },
- { role: "assistant", content: "Message 4", ts: 4, condenseParent: "summary-1" },
- // summary1 with preserved tool_use
- summaryMessage,
- // Messages after first condensation
- { role: "user", content: "Message 6", ts: 6 },
- { role: "assistant", content: "Message 7", ts: 7 },
- { role: "user", content: "Message 8", ts: 8 },
- { role: "assistant", content: "Message 9", ts: 9 },
- { role: "user", content: "Message 10", ts: 10 },
- { role: "assistant", content: "Message 11", ts: 11 },
- // This user message has tool_result referencing the tool_use in summary1
- {
- role: "user",
- content: [toolResultBlock, { type: "text" as const, text: "Continue" }],
- ts: 12,
- },
- { role: "assistant", content: "Message 13", ts: 13 },
- { role: "user", content: "Message 14", ts: 14 },
- ]
-
- // Second condensation: keepCount=3 means we keep messages 12, 13, 14
- // startIndex = 14 - 3 = 11
- // Old bounded search would only look at messages[8:11] = [msg9, msg10, msg11]
- // The tool_use in summary1 at index 4 would NOT be found!
- const result = getKeepMessagesWithToolBlocks(messages, 3)
-
- expect(result.keepMessages).toHaveLength(3)
- expect(result.keepMessages[0].ts).toBe(12) // Has tool_result
- expect(result.keepMessages[1].ts).toBe(13)
- expect(result.keepMessages[2].ts).toBe(14)
-
- // With the fix, we search the ENTIRE condensed region (messages[0:11])
- // and find the tool_use in summary1
- expect(result.toolUseBlocksToPreserve).toHaveLength(1)
- expect(result.toolUseBlocksToPreserve[0].id).toBe("toolu_in_summary")
- })
-
- 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",
- }
-
- 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)
})
})
@@ -667,7 +252,7 @@ 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 (preserves original first user message when needed)", () => {
const messages: ApiMessage[] = [
{ role: "user", content: "Hello", ts: 1 },
{ role: "assistant", content: "Hi there", ts: 2 },
@@ -685,7 +270,7 @@ describe("getMessagesSinceLastSummary", () => {
])
})
- 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 },
@@ -706,6 +291,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", () => {
@@ -748,18 +710,9 @@ 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(
- messages,
- mockApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- )
+ const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId)
expect(result.messages).toEqual(messages)
expect(result.cost).toBe(0)
expect(result.summary).toBe("")
@@ -768,33 +721,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 },
@@ -805,55 +732,93 @@ describe("summarizeConversation", () => {
{ role: "user", content: "Tell me more", ts: 7 },
]
- const result = await summarizeConversation(
- messages,
- mockApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- )
+ const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId)
// 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, mockApiHandler, 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, mockApiHandler, 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[] = [
@@ -881,13 +846,7 @@ describe("summarizeConversation", () => {
return messages.map(({ role, content }: { role: string; content: any }) => ({ role, content }))
})
- const result = await summarizeConversation(
- messages,
- mockApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- )
+ const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId)
// Should return original messages when summary is empty
expect(result.messages).toEqual(messages)
@@ -908,24 +867,27 @@ describe("summarizeConversation", () => {
{ role: "user", content: "Tell me more", ts: 7 },
]
- await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId, DEFAULT_PREV_CONTEXT_TOKENS)
+ await summarizeConversation(messages, mockApiHandler, 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 },
@@ -937,7 +899,7 @@ describe("summarizeConversation", () => {
{ role: "user", content: "Newest", ts: 7 },
]
- await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId, DEFAULT_PREV_CONTEXT_TOKENS)
+ await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId)
const mockCallArgs = (maybeRemoveImageBlocks as Mock).mock.calls[0][0] as any[]
@@ -975,66 +937,19 @@ describe("summarizeConversation", () => {
// Override the mock for this test
mockApiHandler.createMessage = vi.fn().mockReturnValue(streamWithUsage) as any
- const result = await summarizeConversation(
- messages,
- mockApiHandler,
- systemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- )
+ const result = await summarizeConversation(messages, mockApiHandler, systemPrompt, taskId)
- // 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 },
@@ -1054,79 +969,27 @@ 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(
- messages,
- mockApiHandler,
- defaultSystemPrompt,
- taskId,
- prevContextTokens,
- )
+ const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId)
- // 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 },
@@ -1137,14 +1000,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
@@ -1155,18 +1012,9 @@ describe("summarizeConversation", () => {
const mockError = vi.fn()
console.error = mockError
- const result = await summarizeConversation(
- messages,
- invalidMainHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false,
- undefined,
- invalidCondensingHandler,
- )
+ const result = await summarizeConversation(messages, invalidHandler, defaultSystemPrompt, taskId)
- // 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("")
@@ -1174,395 +1022,56 @@ 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 }
- })()
+ const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId)
- mockApiHandler.createMessage = vi.fn().mockReturnValue(streamWithUsage) as any
- mockApiHandler.countTokens = vi.fn().mockImplementation(() => Promise.resolve(50)) as any
-
- const result = await summarizeConversation(
- messages,
- mockApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false, // isAutomaticTrigger
- undefined, // customCondensingPrompt
- undefined, // condensingApiHandler
- true, // useNativeTools - required for tool_use block preservation
- )
-
- // Find the summary message
const summaryMessage = result.messages.find((m) => m.isSummary)
expect(summaryMessage).toBeDefined()
- expect(summaryMessage!.role).toBe("assistant")
- expect(summaryMessage!.isSummary).toBe(true)
- expect(Array.isArray(summaryMessage!.content)).toBe(true)
+ const condenseId = summaryMessage!.condenseId
- // 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()
+ // 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 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",
- }
-
+ 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: "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 },
+ { 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 },
]
- 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 }
- })()
+ const result = await summarizeConversation(messages, mockApiHandler, defaultSystemPrompt, taskId)
- 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 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"])
- })
-
- it("should preserve reasoning blocks in summary message for DeepSeek/Z.ai interleaved thinking", async () => {
- const reasoningBlock = {
- type: "reasoning" as const,
- text: "Let me think about this step by step...",
- }
- const toolUseBlock = {
- type: "tool_use" as const,
- id: "toolu_deepseek_reason",
- name: "read_file",
- input: { path: "test.txt" },
- }
- const toolResultBlock = {
- type: "tool_result" as const,
- tool_use_id: "toolu_deepseek_reason",
- content: "file contents",
- }
-
- const messages: ApiMessage[] = [
- { role: "user", content: "Hello", ts: 1 },
- { role: "assistant", content: "Let me help", ts: 2 },
- { role: "user", content: "Please read the file", ts: 3 },
- {
- role: "assistant",
- // DeepSeek stores reasoning as content blocks alongside tool_use
- content: [reasoningBlock as any, { type: "text" as const, text: "Reading file..." }, toolUseBlock],
- ts: 4,
- },
- {
- role: "user",
- content: [toolResultBlock, { type: "text" as const, text: "Continue" }],
- ts: 5,
- },
- { role: "assistant", content: "Got it, the file says...", ts: 6 },
- { role: "user", content: "Thanks", ts: 7 },
- ]
-
- // Create a stream with usage information
- const streamWithUsage = (async function* () {
- yield { type: "text" as const, text: "Summary of conversation" }
- yield { type: "usage" as const, totalCost: 0.05, outputTokens: 100 }
- })()
-
- mockApiHandler.createMessage = vi.fn().mockReturnValue(streamWithUsage) as any
- mockApiHandler.countTokens = vi.fn().mockImplementation(() => Promise.resolve(50)) as any
-
- const result = await summarizeConversation(
- messages,
- mockApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false, // isAutomaticTrigger
- undefined, // customCondensingPrompt
- undefined, // condensingApiHandler
- true, // useNativeTools - required for tool_use block preservation
- )
-
- // Find the summary message
- const summaryMessage = result.messages.find((m) => m.isSummary)
- expect(summaryMessage).toBeDefined()
- expect(summaryMessage!.role).toBe("assistant")
- expect(summaryMessage!.isSummary).toBe(true)
- expect(Array.isArray(summaryMessage!.content)).toBe(true)
-
- // Content should be [synthetic reasoning, preserved reasoning, text block, tool_use block]
- // - Synthetic reasoning is always added for DeepSeek-reasoner compatibility
- // - Preserved reasoning from the condensed assistant message
- // This order ensures reasoning_content is always present for DeepSeek/Z.ai
- const content = summaryMessage!.content as Anthropic.Messages.ContentBlockParam[]
- expect(content).toHaveLength(4)
-
- // First block should be synthetic reasoning
- expect((content[0] as any).type).toBe("reasoning")
- expect((content[0] as any).text).toContain("Condensing conversation context")
-
- // Second block should be preserved reasoning from the condensed message
- expect((content[1] as any).type).toBe("reasoning")
- expect((content[1] as any).text).toBe("Let me think about this step by step...")
-
- // Third block should be text (the summary)
- expect(content[2].type).toBe("text")
- expect((content[2] as Anthropic.Messages.TextBlockParam).text).toBe("Summary of conversation")
-
- // Fourth block should be tool_use
- expect(content[3].type).toBe("tool_use")
- expect((content[3] as Anthropic.Messages.ToolUseBlockParam).id).toBe("toolu_deepseek_reason")
-
- expect(result.error).toBeUndefined()
- })
-
- it("should include synthetic reasoning block in summary for DeepSeek-reasoner compatibility even without tool_use blocks", async () => {
- // This test verifies the fix for the DeepSeek-reasoner 400 error:
- // "Missing `reasoning_content` field in the assistant message at message index 1"
- // DeepSeek-reasoner requires reasoning_content on ALL assistant messages, not just those with tool_calls.
- // After condensation, the summary becomes an assistant message that needs reasoning_content.
- const messages: ApiMessage[] = [
- { role: "user", content: "Tell me a joke", ts: 1 },
- { role: "assistant", content: "Why did the programmer quit?", ts: 2 },
- { role: "user", content: "I don't know, why?", ts: 3 },
- { role: "assistant", content: "He didn't get arrays!", ts: 4 },
- { role: "user", content: "Another one please", ts: 5 },
- { role: "assistant", content: "Why do programmers prefer dark mode?", ts: 6 },
- { role: "user", content: "Why?", ts: 7 },
- ]
-
- // Create a stream with usage information (no tool calls in this conversation)
- const streamWithUsage = (async function* () {
- yield { type: "text" as const, text: "Summary: User requested jokes." }
- yield { type: "usage" as const, totalCost: 0.05, outputTokens: 100 }
- })()
-
- mockApiHandler.createMessage = vi.fn().mockReturnValue(streamWithUsage) as any
- mockApiHandler.countTokens = vi.fn().mockImplementation(() => Promise.resolve(50)) as any
-
- const result = await summarizeConversation(
- messages,
- mockApiHandler,
- defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false, // isAutomaticTrigger
- undefined, // customCondensingPrompt
- undefined, // condensingApiHandler
- false, // useNativeTools - not using tools in this test
- )
-
- // Find the summary message
- const summaryMessage = result.messages.find((m) => m.isSummary)
- expect(summaryMessage).toBeDefined()
- expect(summaryMessage!.role).toBe("assistant")
- expect(summaryMessage!.isSummary).toBe(true)
-
- // CRITICAL: Content must be an array with a synthetic reasoning block
- // This is required for DeepSeek-reasoner which needs reasoning_content on all assistant messages
- expect(Array.isArray(summaryMessage!.content)).toBe(true)
- const content = summaryMessage!.content as any[]
-
- // Should have [synthetic reasoning, text]
- expect(content).toHaveLength(2)
- expect(content[0].type).toBe("reasoning")
- expect(content[0].text).toContain("Condensing conversation context")
- expect(content[1].type).toBe("text")
- expect(content[1].text).toBe("Summary: User requested jokes.")
-
- expect(result.error).toBeUndefined()
+ // 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[] = [
@@ -1582,7 +1091,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* () {
@@ -1605,29 +1114,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
})
/**
@@ -1640,16 +1126,19 @@ describe("summarizeConversation with custom settings", () => {
sampleMessages,
mockMainApiHandler,
defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
+ localTaskId,
false,
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)
})
/**
@@ -1657,20 +1146,15 @@ 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(sampleMessages, mockMainApiHandler, defaultSystemPrompt, localTaskId, false, " ")
- // 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()
@@ -1678,94 +1162,18 @@ describe("summarizeConversation with custom settings", () => {
sampleMessages,
mockMainApiHandler,
defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
+ localTaskId,
false,
- undefined, // No custom prompt
+ undefined,
)
- // 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")
})
/**
@@ -1776,66 +1184,37 @@ describe("summarizeConversation with custom settings", () => {
sampleMessages,
mockMainApiHandler,
defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
+ localTaskId,
false,
"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 () => {
+ it("should capture telemetry with isAutomaticTrigger flag", async () => {
await summarizeConversation(
sampleMessages,
mockMainApiHandler,
defaultSystemPrompt,
- taskId,
- DEFAULT_PREV_CONTEXT_TOKENS,
- false,
- undefined,
- mockCondensingApiHandler,
- )
-
- // Verify telemetry was called with custom API handler 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,
+ localTaskId,
true, // isAutomaticTrigger
"Custom prompt",
- mockCondensingApiHandler,
)
- // Verify telemetry was called with both flags
+ // Verify telemetry was called with isAutomaticTrigger flag
expect(TelemetryService.instance.captureContextCondensed).toHaveBeenCalledWith(
- taskId,
+ localTaskId,
true, // isAutomaticTrigger
true, // usedCustomPrompt
- true, // usedCustomApiHandler
)
})
})
diff --git a/src/core/condense/__tests__/rewind-after-condense.spec.ts b/src/core/condense/__tests__/rewind-after-condense.spec.ts
index f5f1a09380..84fdb63ca8 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", () => {
@@ -131,44 +131,32 @@ describe("Rewind After Condense - Issue #8295", () => {
it("should reactivate condensed messages when their summary is deleted via truncation", () => {
const condenseId = "summary-abc"
- // Simulate a conversation after condensing
+ // Simulate a conversation after condensing (all prior messages tagged)
const fullHistory: ApiMessage[] = [
- { role: "user", content: "Initial task", ts: 1 },
+ { role: "user", content: "Initial task", ts: 1, condenseParent: condenseId },
{ role: "assistant", content: "Working on it", ts: 2, condenseParent: condenseId },
{ role: "user", content: "Continue", ts: 3, condenseParent: condenseId },
- { role: "assistant", content: "Summary of work so far", ts: 4, isSummary: true, condenseId },
- { role: "user", content: "Now do this", ts: 5 },
- { role: "assistant", content: "Done", ts: 6 },
- { role: "user", content: "And this", ts: 7 },
- { role: "assistant", content: "Also done", ts: 8 },
+ { role: "user", content: "Summary of work so far", ts: 4, isSummary: true, condenseId },
]
// Verify effective history before truncation
const effectiveBefore = getEffectiveApiHistory(fullHistory)
- // Should be: first message, summary, last 4 messages
- expect(effectiveBefore.length).toBe(6)
+ // Should be: summary only
+ expect(effectiveBefore.length).toBe(1)
- // Simulate rewind: user truncates back to message ts=4 (keeping 0-3)
- const truncatedHistory = fullHistory.slice(0, 4) // Keep first, condensed1, condensed2, summary
+ // Simulate rewind: delete the summary message
+ const withoutSummary = fullHistory.filter((m) => !m.isSummary)
+ const cleanedAfterDeletingSummary = cleanupAfterTruncation(withoutSummary)
+ for (const msg of cleanedAfterDeletingSummary) {
+ expect(msg.condenseParent).toBeUndefined()
+ }
- // After truncation, the summary is still there, so condensed messages remain condensed
- const cleanedAfterKeepingSummary = cleanupAfterTruncation(truncatedHistory)
- expect(cleanedAfterKeepingSummary[1].condenseParent).toBe(condenseId)
- expect(cleanedAfterKeepingSummary[2].condenseParent).toBe(condenseId)
-
- // Now simulate a more aggressive rewind: delete back to message ts=2
- const aggressiveTruncate = fullHistory.slice(0, 2) // Keep only first message and first response
-
- // The condensed messages should now be reactivated since summary is gone
- const cleanedAfterDeletingSummary = cleanupAfterTruncation(aggressiveTruncate)
- expect(cleanedAfterDeletingSummary[1].condenseParent).toBeUndefined()
-
- // Verify effective history after cleanup
+ // Verify effective history after cleanup: all messages should be visible now
const effectiveAfterCleanup = getEffectiveApiHistory(cleanedAfterDeletingSummary)
- // Now both messages should be active (no condensed filtering)
- expect(effectiveAfterCleanup.length).toBe(2)
+ expect(effectiveAfterCleanup.length).toBe(3)
expect(effectiveAfterCleanup[0].content).toBe("Initial task")
expect(effectiveAfterCleanup[1].content).toBe("Working on it")
+ expect(effectiveAfterCleanup[2].content).toBe("Continue")
})
it("should properly restore context after rewind when summary was deleted", () => {
@@ -206,10 +194,11 @@ describe("Rewind After Condense - Issue #8295", () => {
expect(effectiveAfter.length).toBe(5) // All messages visible
})
- it("should hide condensed messages when their summary still exists", () => {
+ it("should hide condensed messages when their summary still exists (fresh start)", () => {
const condenseId = "summary-exists"
- // Scenario: Messages were condensed and summary exists - condensed messages should be hidden
+ // Scenario: Messages were condensed and summary exists - fresh start model returns
+ // only the summary and messages after it, NOT messages before the summary
const messages: ApiMessage[] = [
{ role: "user", content: "Start", ts: 1 },
{ role: "assistant", content: "Response 1", ts: 2, condenseParent: condenseId },
@@ -218,12 +207,13 @@ describe("Rewind After Condense - Issue #8295", () => {
{ role: "user", content: "After summary", ts: 5 },
]
- // Effective history should hide condensed messages since summary exists
+ // Fresh start model: effective history is summary + messages after it
+ // "Start" is NOT included because it's before the summary
const effective = getEffectiveApiHistory(messages)
- expect(effective.length).toBe(3) // Start, Summary, After summary
- expect(effective[0].content).toBe("Start")
- expect(effective[1].content).toBe("Summary")
- expect(effective[2].content).toBe("After summary")
+ expect(effective.length).toBe(2) // Summary, After summary (NOT Start)
+ expect(effective[0].content).toBe("Summary")
+ expect(effective[0].isSummary).toBe(true)
+ expect(effective[1].content).toBe("After summary")
// cleanupAfterTruncation should NOT clear condenseParent since summary exists
const cleaned = cleanupAfterTruncation(messages)
@@ -315,8 +305,7 @@ describe("Rewind After Condense - Issue #8295", () => {
/**
* These tests verify that the correct user and assistant messages are preserved
* and sent to the LLM after condense operations. With N_MESSAGES_TO_KEEP = 3,
- * condense should always preserve:
- * - The first message (never condensed)
+ * condense should always preserve (for effective history):
* - The active summary
* - The last 3 kept messages
*/
@@ -332,7 +321,7 @@ describe("Rewind After Condense - Issue #8295", () => {
// - summary inserted with ts = msg8.ts - 1
// - msg8, msg9, msg10 kept
const storageAfterCondense: ApiMessage[] = [
- { role: "user", content: "Task: Build a feature", ts: 100 },
+ { role: "user", content: "Task: Build a feature", ts: 100, condenseParent: condenseId },
{ role: "assistant", content: "I'll help with that", ts: 200, condenseParent: condenseId },
{ role: "user", content: "Start with the API", ts: 300, condenseParent: condenseId },
{ role: "assistant", content: "Creating API endpoints", ts: 400, condenseParent: condenseId },
@@ -355,28 +344,24 @@ describe("Rewind After Condense - Issue #8295", () => {
const effective = getEffectiveApiHistory(storageAfterCondense)
- // Should send exactly 5 messages to LLM:
- // 1. First message (user) - preserved
- // 2. Summary (assistant)
- // 3-5. Last 3 kept messages
- expect(effective.length).toBe(5)
+ // Should send exactly 4 messages to LLM:
+ // 1. Summary (assistant)
+ // 2-4. Last 3 kept messages
+ expect(effective.length).toBe(4)
// Verify exact order and content
- expect(effective[0].role).toBe("user")
- expect(effective[0].content).toBe("Task: Build a feature")
+ expect(effective[0].role).toBe("assistant")
+ expect(effective[0].isSummary).toBe(true)
+ expect(effective[0].content).toBe("Summary: Built API with validation, working on tests")
expect(effective[1].role).toBe("assistant")
- expect(effective[1].isSummary).toBe(true)
- expect(effective[1].content).toBe("Summary: Built API with validation, working on tests")
+ expect(effective[1].content).toBe("Writing unit tests now")
- expect(effective[2].role).toBe("assistant")
- expect(effective[2].content).toBe("Writing unit tests now")
+ expect(effective[2].role).toBe("user")
+ expect(effective[2].content).toBe("Include edge cases")
- expect(effective[3].role).toBe("user")
- expect(effective[3].content).toBe("Include edge cases")
-
- expect(effective[4].role).toBe("assistant")
- expect(effective[4].content).toBe("Added edge case tests")
+ expect(effective[3].role).toBe("assistant")
+ expect(effective[3].content).toBe("Added edge case tests")
// Verify condensed messages are NOT in effective history
const condensedContents = ["I'll help with that", "Start with the API", "Creating API endpoints"]
@@ -396,8 +381,8 @@ describe("Rewind After Condense - Issue #8295", () => {
//
// Storage after double condense:
const storageAfterDoubleCondense: ApiMessage[] = [
- // First message - never condensed
- { role: "user", content: "Initial task: Build a full app", ts: 100 },
+ // First message - condensed during the first condense
+ { role: "user", content: "Initial task: Build a full app", ts: 100, condenseParent: condenseId1 },
// Messages from first condense (tagged with condenseId1)
{ role: "assistant", content: "Starting the project", ts: 200, condenseParent: condenseId1 },
@@ -446,29 +431,25 @@ describe("Rewind After Condense - Issue #8295", () => {
const effective = getEffectiveApiHistory(storageAfterDoubleCondense)
- // Should send exactly 5 messages to LLM:
- // 1. First message (user) - preserved
- // 2. Summary2 (assistant) - the ACTIVE summary
- // 3-5. Last 3 kept messages
- expect(effective.length).toBe(5)
+ // Should send exactly 4 messages to LLM:
+ // 1. Summary2 (assistant) - the ACTIVE summary
+ // 2-4. Last 3 kept messages
+ expect(effective.length).toBe(4)
// Verify exact order and content
- expect(effective[0].role).toBe("user")
- expect(effective[0].content).toBe("Initial task: Build a full app")
+ expect(effective[0].role).toBe("assistant")
+ expect(effective[0].isSummary).toBe(true)
+ expect(effective[0].condenseId).toBe(condenseId2) // Must be the SECOND summary
+ expect(effective[0].content).toContain("Summary2")
expect(effective[1].role).toBe("assistant")
- expect(effective[1].isSummary).toBe(true)
- expect(effective[1].condenseId).toBe(condenseId2) // Must be the SECOND summary
- expect(effective[1].content).toContain("Summary2")
+ expect(effective[1].content).toBe("Writing integration tests")
- expect(effective[2].role).toBe("assistant")
- expect(effective[2].content).toBe("Writing integration tests")
+ expect(effective[2].role).toBe("user")
+ expect(effective[2].content).toBe("Test the auth flow")
- expect(effective[3].role).toBe("user")
- expect(effective[3].content).toBe("Test the auth flow")
-
- expect(effective[4].role).toBe("assistant")
- expect(effective[4].content).toBe("Auth tests passing")
+ expect(effective[3].role).toBe("assistant")
+ expect(effective[3].content).toBe("Auth tests passing")
// Verify Summary1 is NOT in effective history (it's tagged with condenseParent)
const summary1 = effective.find((m) => m.content?.toString().includes("Summary1"))
@@ -493,7 +474,7 @@ describe("Rewind After Condense - Issue #8295", () => {
// Verify that after condense, the effective history maintains proper
// user/assistant message alternation (important for API compatibility)
const storage: ApiMessage[] = [
- { role: "user", content: "Start task", ts: 100 },
+ { role: "user", content: "Start task", ts: 100, condenseParent: condenseId },
{ role: "assistant", content: "Response 1", ts: 200, condenseParent: condenseId },
{ role: "user", content: "Continue", ts: 300, condenseParent: condenseId },
{ role: "assistant", content: "Summary text", ts: 399, isSummary: true, condenseId },
@@ -505,22 +486,21 @@ describe("Rewind After Condense - Issue #8295", () => {
const effective = getEffectiveApiHistory(storage)
- // Verify the sequence: user, assistant(summary), assistant, user, assistant
+ // Verify the sequence: assistant(summary), assistant, user, assistant
// Note: Having two assistant messages in a row (summary + next response) is valid
// because the summary replaces what would have been multiple messages
- expect(effective[0].role).toBe("user")
+ expect(effective[0].role).toBe("assistant")
+ expect(effective[0].isSummary).toBe(true)
expect(effective[1].role).toBe("assistant")
- expect(effective[1].isSummary).toBe(true)
- expect(effective[2].role).toBe("assistant")
- expect(effective[3].role).toBe("user")
- expect(effective[4].role).toBe("assistant")
+ expect(effective[2].role).toBe("user")
+ expect(effective[3].role).toBe("assistant")
})
it("should preserve timestamps in chronological order in effective history", () => {
const condenseId = "summary-timestamps"
const storage: ApiMessage[] = [
- { role: "user", content: "First", ts: 100 },
+ { role: "user", content: "First", ts: 100, condenseParent: condenseId },
{ role: "assistant", content: "Condensed", ts: 200, condenseParent: condenseId },
{ role: "assistant", content: "Summary", ts: 299, isSummary: true, condenseId },
{ role: "user", content: "Kept 1", ts: 300 },
diff --git a/src/core/condense/index.ts b/src/core/condense/index.ts
index dc18c61876..0c92087aff 100644
--- a/src/core/condense/index.ts
+++ b/src/core/condense/index.ts
@@ -4,200 +4,114 @@ import crypto from "crypto"
import { TelemetryService } from "@roo-code/telemetry"
import { t } from "../../i18n"
-import { ApiHandler } from "../../api"
+import { ApiHandler, ApiHandlerCreateMessageMetadata } from "../../api"
import { ApiMessage } from "../task-persistence/apiMessages"
import { maybeRemoveImageBlocks } from "../../api/transform/image-cleaning"
import { findLast } from "../../shared/array"
+import { supportPrompt } from "../../shared/support-prompt"
-/**
- * Checks if a message contains tool_result blocks.
- * 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")
-}
-
-/**
- * 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 ENTIRE
- * condensed region to find the matching tool_use blocks by ID.
- * These tool_use blocks will be appended to the summary message to maintain proper pairing.
- *
- * IMPORTANT: The search must cover the entire condensed region (from index 0 to startIndex-1),
- * not just a bounded window. After multiple condensations, tool_use blocks can be preserved in
- * earlier summary messages that are outside a bounded search window.
- * See: https://linear.app/roocode/issue/ROO-520
- *
- * 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 ENTIRE condensed region (from index 0 to startIndex-1)
- // This is critical because after multiple condensations, tool_use blocks may be
- // preserved in earlier summary messages that are outside a bounded search window.
- const messagesToSearch = messages.slice(0, startIndex)
-
- // 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
@@ -205,31 +119,35 @@ export type SummarizeResponse = {
cost: number // The cost of the summarization operation
newContextTokens?: number // The number of tokens in the context for the next API request
error?: string // Populated iff the operation fails: error message shown to the user on failure (see Task.ts)
+ errorDetails?: string // Detailed error information including stack trace and API error info
condenseId?: string // The unique ID of the created Summary message, for linking to condense_context clineMessage
}
/**
- * Summarizes the conversation messages using an LLM call
+ * Summarizes the conversation messages using an LLM call.
+ *
+ * This implements the "fresh start" model where:
+ * - The summary becomes a user message (not assistant)
+ * - Post-condense, the model sees only the summary (true fresh start)
+ * - All messages are still stored but tagged with condenseParent
+ * - blocks from the original task are preserved across condensings
+ *
+ * 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().
*
* @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
- *
- * @param {ApiMessage[]} messages - The conversation messages
- * @param {ApiHandler} apiHandler - The API handler to use for token counting (fallback if condensingApiHandler not provided)
+ * @param {ApiHandler} apiHandler - The API handler to use for summarization and token counting
* @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)
+ * @param {ApiHandlerCreateMessageMetadata} metadata - Optional metadata to pass to createMessage (tools, taskId, etc.)
+ * @param {string} environmentDetails - Optional environment details string to include in the summary (only used when isAutomaticTrigger=true)
* @returns {SummarizeResponse} - The result of the summarization operation (see above)
*/
export async function summarizeConversation(
@@ -237,106 +155,120 @@ export async function summarizeConversation(
apiHandler: ApiHandler,
systemPrompt: string,
taskId: string,
- prevContextTokens: number,
isAutomaticTrigger?: boolean,
customCondensingPrompt?: string,
- condensingApiHandler?: ApiHandler,
- useNativeTools?: boolean,
+ metadata?: ApiHandlerCreateMessageMetadata,
+ environmentDetails?: string,
): Promise {
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,
}
}
@@ -347,105 +279,96 @@ 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 as a separate text 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 }
-
- 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 }
}
@@ -485,8 +408,11 @@ export function getMessagesSinceLastSummary(messages: ApiMessage[]): ApiMessage[
/**
* Filters the API conversation history to get the "effective" messages to send to the API.
- * Messages with a condenseParent that points to an existing summary are filtered out,
- * as they have been replaced by that summary.
+ *
+ * Fresh Start Model:
+ * - When a summary exists, return only messages from the summary onwards (fresh start)
+ * - Messages with a condenseParent pointing to an existing summary are filtered out
+ *
* Messages with a truncationParent that points to an existing truncation marker are also filtered out,
* as they have been hidden by sliding window truncation.
*
@@ -497,6 +423,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
@@ -513,7 +504,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..7c5db2d510 100644
--- a/src/core/context-management/__tests__/context-management.spec.ts
+++ b/src/core/context-management/__tests__/context-management.spec.ts
@@ -617,11 +617,10 @@ describe("Context Management", () => {
mockApiHandler,
"System prompt",
taskId,
- 70001,
- true,
+ true, // isAutomaticTrigger
undefined, // customCondensingPrompt
- undefined, // condensingApiHandler
- undefined, // useNativeTools
+ undefined, // metadata
+ undefined, // environmentDetails
)
// Verify the result contains the summary information
@@ -793,11 +792,10 @@ describe("Context Management", () => {
mockApiHandler,
"System prompt",
taskId,
- 60000,
- true,
+ true, // isAutomaticTrigger
undefined, // customCondensingPrompt
- undefined, // condensingApiHandler
- undefined, // useNativeTools
+ undefined, // metadata
+ undefined, // environmentDetails
)
// Verify the result contains the summary information
diff --git a/src/core/context-management/index.ts b/src/core/context-management/index.ts
index a94a53c9d5..250dbeccbe 100644
--- a/src/core/context-management/index.ts
+++ b/src/core/context-management/index.ts
@@ -3,7 +3,7 @@ import crypto from "crypto"
import { TelemetryService } from "@roo-code/telemetry"
-import { ApiHandler } from "../../api"
+import { ApiHandler, ApiHandlerCreateMessageMetadata } from "../../api"
import { MAX_CONDENSE_THRESHOLD, MIN_CONDENSE_THRESHOLD, summarizeConversation, SummarizeResponse } from "../condense"
import { ApiMessage } from "../task-persistence/apiMessages"
import { ANTHROPIC_DEFAULT_MAX_TOKENS } from "@roo-code/types"
@@ -216,10 +216,12 @@ 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
}
export type ContextManagementResult = SummarizeResponse & {
@@ -246,12 +248,13 @@ export async function manageContext({
systemPrompt,
taskId,
customCondensingPrompt,
- condensingApiHandler,
profileThresholds,
currentProfileId,
- useNativeTools,
+ metadata,
+ environmentDetails,
}: 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
@@ -299,14 +302,14 @@ export async function manageContext({
apiHandler,
systemPrompt,
taskId,
- prevContextTokens,
true, // automatic trigger
customCondensingPrompt,
- condensingApiHandler,
- useNativeTools,
+ metadata,
+ environmentDetails,
)
if (result.error) {
error = result.error
+ errorDetails = result.errorDetails
cost = result.cost
} else {
return { ...result, prevContextTokens }
@@ -349,11 +352,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/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..39deb3f495 100644
--- a/src/core/environment/getEnvironmentDetails.ts
+++ b/src/core/environment/getEnvironmentDetails.ts
@@ -8,8 +8,6 @@ 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"
@@ -236,26 +234,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/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..70cccc68f0 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,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.
@@ -385,7 +53,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..ee604b3036 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,309 +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
-
-
-## 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.
@@ -343,7 +53,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..70cccc68f0 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,350 +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_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:
+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.
@@ -384,7 +53,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
index acc36d1ffd..51fd18172b 100644
--- 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
@@ -10,400 +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"
-}
-
-
-
-## 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:
+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.
@@ -453,7 +72,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/partial-reads-enabled.snap b/src/core/prompts/__tests__/__snapshots__/add-custom-instructions/partial-reads-enabled.snap
index ac93623fda..70cccc68f0 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,356 +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. 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:
+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.
@@ -390,7 +53,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..5305987e28 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,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.
@@ -385,7 +53,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 +102,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..5305987e28 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,436 +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"
-}
-
-
-
-## 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:
+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.
@@ -470,7 +53,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 +102,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..5305987e28 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.
@@ -385,7 +53,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 +102,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..5305987e28 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.
@@ -473,7 +53,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 +102,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..5305987e28 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.
@@ -385,7 +53,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 +102,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..5305987e28 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,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.
@@ -385,7 +53,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 +102,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..baa8d519d8 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,400 +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"
-}
-
-
-
-## 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:
+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.
@@ -453,7 +72,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 +121,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..5305987e28 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,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.
@@ -385,7 +53,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 +102,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..52b3496370 100644
--- a/src/core/prompts/__tests__/add-custom-instructions.spec.ts
+++ b/src/core/prompts/__tests__/add-custom-instructions.spec.ts
@@ -210,7 +210,6 @@ describe("addCustomInstructions", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
undefined, // language
@@ -233,7 +232,6 @@ describe("addCustomInstructions", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
undefined, // language
@@ -258,7 +256,6 @@ describe("addCustomInstructions", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
undefined, // language
@@ -284,7 +281,6 @@ describe("addCustomInstructions", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
false, // enableMcpServerCreation
undefined, // language
@@ -308,7 +304,6 @@ describe("addCustomInstructions", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
undefined, // language
diff --git a/src/core/prompts/__tests__/custom-system-prompt.spec.ts b/src/core/prompts/__tests__/custom-system-prompt.spec.ts
index 6106e16174..5399b92c65 100644
--- a/src/core/prompts/__tests__/custom-system-prompt.spec.ts
+++ b/src/core/prompts/__tests__/custom-system-prompt.spec.ts
@@ -104,7 +104,6 @@ describe("File-Based Custom System Prompt", () => {
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
undefined, // language
@@ -142,7 +141,6 @@ describe("File-Based Custom System Prompt", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
undefined, // language
@@ -188,7 +186,6 @@ describe("File-Based Custom System Prompt", () => {
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
undefined, // language
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..d171e13507 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,7 +225,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
undefined, // language
@@ -250,7 +247,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
undefined, // language
@@ -275,7 +271,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
undefined, // language
@@ -298,7 +293,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
undefined, // language
@@ -321,7 +315,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes,
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
undefined, // language
@@ -331,79 +324,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,7 +363,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
true, // enableMcpServerCreation
undefined, // language
@@ -504,7 +423,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
customModes, // customModes
"Global instructions", // globalCustomInstructions
- undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
undefined, // language
@@ -542,7 +460,6 @@ describe("SYSTEM_PROMPT", () => {
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
false, // enableMcpServerCreation
undefined, // language
@@ -575,7 +492,6 @@ describe("SYSTEM_PROMPT", () => {
customModePrompts, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
undefined, // experiments
false, // enableMcpServerCreation
undefined, // language
@@ -593,7 +509,6 @@ describe("SYSTEM_PROMPT", () => {
todoListEnabled: false,
useAgentRules: true,
newTaskRequireTodos: false,
- toolProtocol: "xml" as const,
}
const prompt = await SYSTEM_PROMPT(
@@ -607,7 +522,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
undefined, // language
@@ -627,7 +541,6 @@ describe("SYSTEM_PROMPT", () => {
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: false,
- toolProtocol: "xml" as const,
}
const prompt = await SYSTEM_PROMPT(
@@ -641,7 +554,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
undefined, // language
@@ -650,8 +562,9 @@ describe("SYSTEM_PROMPT", () => {
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 +573,6 @@ describe("SYSTEM_PROMPT", () => {
todoListEnabled: true,
useAgentRules: true,
newTaskRequireTodos: false,
- toolProtocol: "xml" as const,
}
const prompt = await SYSTEM_PROMPT(
@@ -674,7 +586,6 @@ describe("SYSTEM_PROMPT", () => {
undefined, // customModePrompts
undefined, // customModes
undefined, // globalCustomInstructions
- undefined, // diffEnabled
experiments,
true, // enableMcpServerCreation
undefined, // language
@@ -683,17 +594,17 @@ describe("SYSTEM_PROMPT", () => {
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,79 +618,6 @@ 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
@@ -794,17 +632,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")
@@ -820,44 +654,6 @@ describe("SYSTEM_PROMPT", () => {
expect(prompt).toContain("SYSTEM INFORMATION")
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
index 80f69b0802..9623aae0cd 100644
--- a/src/core/prompts/instructions/create-mode.ts
+++ b/src/core/prompts/instructions/create-mode.ts
@@ -17,7 +17,6 @@ Custom modes can be configured in two ways:
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:
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\n${feedback}\n`
- },
+ toolDeniedWithFeedback: (feedback?: string) =>
+ JSON.stringify({
+ status: "denied",
+ feedback,
+ }),
- toolApprovedWithFeedback: (feedback?: string, protocol?: ToolProtocol) => {
- if (isNativeProtocol(protocol ?? TOOL_PROTOCOL.XML)) {
- return JSON.stringify({
- status: "approved",
- message: "The user approved this operation and provided the following context",
- feedback: feedback,
- })
- }
- return `The user approved this operation and provided the following context:\n\n${feedback}\n`
- },
+ toolApprovedWithFeedback: (feedback?: string) =>
+ JSON.stringify({
+ status: "approved",
+ feedback,
+ }),
- toolError: (error?: string, protocol?: ToolProtocol) => {
- if (isNativeProtocol(protocol ?? TOOL_PROTOCOL.XML)) {
- return JSON.stringify({
- status: "error",
- message: "The tool execution failed",
- error: error,
- })
- }
- return `The tool execution failed with the following error:\n\n${error}\n`
- },
+ toolError: (error?: string) =>
+ JSON.stringify({
+ status: "error",
+ message: "The tool execution failed",
+ error,
+ }),
- rooIgnoreError: (path: string, protocol?: ToolProtocol) => {
- if (isNativeProtocol(protocol ?? TOOL_PROTOCOL.XML)) {
- return JSON.stringify({
- status: "error",
- type: "access_denied",
- message: "Access blocked by .rooignore",
- path: path,
- suggestion: "Try to continue without this file, or ask the user to update the .rooignore file",
- })
- }
- return `Access to ${path} is blocked by the .rooignore file settings. You must try to continue in the task without using this file, or ask the user to update the .rooignore file.`
- },
+ rooIgnoreError: (path: string) =>
+ JSON.stringify({
+ status: "error",
+ type: "access_denied",
+ message: "Access blocked by .rooignore",
+ path,
+ suggestion: "Try to continue without this file, or ask the user to update the .rooignore file",
+ }),
- noToolsUsed: (protocol?: ToolProtocol) => {
- const instructions = getToolInstructionsReminder(protocol)
+ noToolsUsed: () => {
+ const instructions = getToolInstructionsReminder()
return `[ERROR] You did not use a tool in your previous response! Please retry with a tool use.
@@ -77,66 +54,47 @@ Otherwise, if you have not completed the task and do not need additional informa
(This is an automated message, so do not respond to it conversationally.)`
},
- tooManyMistakes: (feedback?: string, protocol?: ToolProtocol) => {
- if (isNativeProtocol(protocol ?? TOOL_PROTOCOL.XML)) {
- return JSON.stringify({
- status: "guidance",
- message: "You seem to be having trouble proceeding",
- feedback: feedback,
- })
- }
- return `You seem to be having trouble proceeding. The user has provided the following feedback to help guide you:\n\n${feedback}\n`
- },
+ tooManyMistakes: (feedback?: string) =>
+ JSON.stringify({
+ status: "guidance",
+ feedback,
+ }),
- missingToolParameterError: (paramName: string, protocol?: ToolProtocol) => {
- const instructions = getToolInstructionsReminder(protocol)
+ missingToolParameterError: (paramName: string) => {
+ const instructions = getToolInstructionsReminder()
return `Missing value for required parameter '${paramName}'. Please retry with complete response.\n\n${instructions}`
},
- invalidMcpToolArgumentError: (serverName: string, toolName: string, protocol?: ToolProtocol) => {
- if (isNativeProtocol(protocol ?? TOOL_PROTOCOL.XML)) {
- return JSON.stringify({
- status: "error",
- type: "invalid_argument",
- message: "Invalid JSON argument",
- server: serverName,
- tool: toolName,
- suggestion: "Please retry with a properly formatted JSON argument",
- })
- }
- return `Invalid JSON argument used with ${serverName} for ${toolName}. Please retry with a properly formatted JSON argument.`
- },
+ invalidMcpToolArgumentError: (serverName: string, toolName: string) =>
+ JSON.stringify({
+ status: "error",
+ type: "invalid_argument",
+ message: "Invalid JSON argument",
+ server: serverName,
+ tool: toolName,
+ suggestion: "Please retry with a properly formatted JSON argument",
+ }),
- unknownMcpToolError: (serverName: string, toolName: string, availableTools: string[], protocol?: ToolProtocol) => {
- if (isNativeProtocol(protocol ?? TOOL_PROTOCOL.XML)) {
- return JSON.stringify({
- status: "error",
- type: "unknown_tool",
- message: "Tool does not exist on server",
- server: serverName,
- tool: toolName,
- available_tools: availableTools.length > 0 ? availableTools : [],
- suggestion: "Please use one of the available tools or check if the server is properly configured",
- })
- }
- const toolsList = availableTools.length > 0 ? availableTools.join(", ") : "No tools available"
- return `Tool '${toolName}' does not exist on server '${serverName}'.\n\nAvailable tools on this server: ${toolsList}\n\nPlease use one of the available tools or check if the server is properly configured.`
- },
+ unknownMcpToolError: (serverName: string, toolName: string, availableTools: string[]) =>
+ JSON.stringify({
+ status: "error",
+ type: "unknown_tool",
+ message: "Tool does not exist on server",
+ server: serverName,
+ tool: toolName,
+ available_tools: availableTools.length > 0 ? availableTools : [],
+ suggestion: "Please use one of the available tools or check if the server is properly configured",
+ }),
- unknownMcpServerError: (serverName: string, availableServers: string[], protocol?: ToolProtocol) => {
- if (isNativeProtocol(protocol ?? TOOL_PROTOCOL.XML)) {
- return JSON.stringify({
- status: "error",
- type: "unknown_server",
- message: "Server is not configured",
- server: serverName,
- available_servers: availableServers.length > 0 ? availableServers : [],
- })
- }
- const serversList = availableServers.length > 0 ? availableServers.join(", ") : "No servers available"
- return `Server '${serverName}' is not configured. Available servers: ${serversList}`
- },
+ unknownMcpServerError: (serverName: string, availableServers: string[]) =>
+ JSON.stringify({
+ status: "error",
+ type: "unknown_server",
+ message: "Server is not configured",
+ server: serverName,
+ available_servers: availableServers.length > 0 ? availableServers : [],
+ }),
toolResult: (
text: string,
@@ -258,26 +216,6 @@ const formatImagesIntoBlocks = (images?: string[]): Anthropic.ImageBlockParam[]
: []
}
-const toolUseInstructionsReminder = `# Reminder: Instructions for Tool Use
-
-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
-...
-
-
-For example, to use the attempt_completion tool:
-
-
-
-I have completed the task...
-
-
-
-Always use the actual tool name as the XML tag name for proper parsing and execution.`
-
const toolUseInstructionsReminderNative = `# Reminder: Instructions for Tool Use
Tools are invoked using the platform's native tool calling mechanism. Each tool requires specific parameters as defined in the tool descriptions. Refer to the tool definitions provided in your system instructions for the correct parameter structure and usage examples.
@@ -285,12 +223,8 @@ Tools are invoked using the platform's native tool calling mechanism. Each tool
Always ensure you provide all required parameters for the tool you wish to use.`
/**
- * Gets the appropriate tool use instructions reminder based on the protocol.
- *
- * @param protocol - Optional tool protocol, defaults to XML if not provided
- * @returns The tool use instructions reminder text
+ * Gets the tool use instructions reminder.
*/
-function getToolInstructionsReminder(protocol?: ToolProtocol): string {
- const effectiveProtocol = protocol ?? TOOL_PROTOCOL.XML
- return isNativeProtocol(effectiveProtocol) ? toolUseInstructionsReminderNative : toolUseInstructionsReminder
+function getToolInstructionsReminder(): string {
+ return toolUseInstructionsReminderNative
}
diff --git a/src/core/prompts/sections/__tests__/tool-use-guidelines.spec.ts b/src/core/prompts/sections/__tests__/tool-use-guidelines.spec.ts
index 3cb3fb51d0..26c1f77d85 100644
--- a/src/core/prompts/sections/__tests__/tool-use-guidelines.spec.ts
+++ b/src/core/prompts/sections/__tests__/tool-use-guidelines.spec.ts
@@ -1,99 +1,66 @@
import { getToolUseGuidelinesSection } from "../tool-use-guidelines"
-import { TOOL_PROTOCOL } from "@roo-code/types"
import { EXPERIMENT_IDS } from "../../../../shared/experiments"
describe("getToolUseGuidelinesSection", () => {
- describe("XML protocol", () => {
+ describe("with MULTIPLE_NATIVE_TOOL_CALLS disabled (default)", () => {
it("should include proper numbered guidelines", () => {
- const guidelines = getToolUseGuidelinesSection(TOOL_PROTOCOL.XML)
+ const guidelines = getToolUseGuidelinesSection()
// Check that all numbered items are present with correct numbering
expect(guidelines).toContain("1. Assess what information")
expect(guidelines).toContain("2. Choose the most appropriate tool")
expect(guidelines).toContain("3. If multiple actions are needed")
- expect(guidelines).toContain("4. Formulate your tool use")
- expect(guidelines).toContain("5. After each tool use")
- expect(guidelines).toContain("6. ALWAYS wait for user confirmation")
+ expect(guidelines).toContain("4. After each tool use")
})
- it("should include XML-specific guidelines", () => {
- const guidelines = getToolUseGuidelinesSection(TOOL_PROTOCOL.XML)
+ it("should include single-tool-per-message guidance when experiment disabled", () => {
+ const guidelines = getToolUseGuidelinesSection({})
- expect(guidelines).toContain("Formulate your tool use using the XML format specified for each tool")
expect(guidelines).toContain("use one tool at a time per message")
+ expect(guidelines).not.toContain("you may use multiple tools in a single message")
+ expect(guidelines).not.toContain("Formulate your tool use using")
expect(guidelines).toContain("ALWAYS wait for user confirmation")
})
- it("should include iterative process guidelines", () => {
- const guidelines = getToolUseGuidelinesSection(TOOL_PROTOCOL.XML)
+ it("should include simplified iterative process guidelines", () => {
+ const guidelines = getToolUseGuidelinesSection()
+ expect(guidelines).toContain("carefully considering the user's response after each tool use")
expect(guidelines).toContain("It is crucial to proceed step-by-step")
- expect(guidelines).toContain("1. Confirm the success of each step before proceeding")
- expect(guidelines).toContain("2. Address any issues or errors that arise immediately")
- expect(guidelines).toContain("3. Adapt your approach based on new information")
- expect(guidelines).toContain("4. Ensure that each action builds correctly")
})
})
- describe("native protocol", () => {
- describe("with MULTIPLE_NATIVE_TOOL_CALLS disabled (default)", () => {
- it("should include proper numbered guidelines", () => {
- const guidelines = getToolUseGuidelinesSection(TOOL_PROTOCOL.NATIVE)
-
- // Check that all numbered items are present with correct numbering
- expect(guidelines).toContain("1. Assess what information")
- expect(guidelines).toContain("2. Choose the most appropriate tool")
- expect(guidelines).toContain("3. If multiple actions are needed")
- expect(guidelines).toContain("4. After each tool use")
+ describe("with MULTIPLE_NATIVE_TOOL_CALLS enabled", () => {
+ it("should include multiple-tools-per-message guidance when experiment enabled", () => {
+ const guidelines = getToolUseGuidelinesSection({
+ [EXPERIMENT_IDS.MULTIPLE_NATIVE_TOOL_CALLS]: true,
})
- it("should include single-tool-per-message guidance when experiment disabled", () => {
- const guidelines = getToolUseGuidelinesSection(TOOL_PROTOCOL.NATIVE, {})
-
- expect(guidelines).toContain("use one tool at a time per message")
- expect(guidelines).not.toContain("you may use multiple tools in a single message")
- expect(guidelines).not.toContain("Formulate your tool use using the XML format")
- expect(guidelines).not.toContain("ALWAYS wait for user confirmation")
- })
-
- it("should include simplified iterative process guidelines", () => {
- const guidelines = getToolUseGuidelinesSection(TOOL_PROTOCOL.NATIVE)
-
- expect(guidelines).toContain("carefully considering the user's response after tool executions")
- // Native protocol doesn't have the step-by-step list
- expect(guidelines).not.toContain("It is crucial to proceed step-by-step")
- })
+ expect(guidelines).toContain("you may use multiple tools in a single message")
+ expect(guidelines).not.toContain("use one tool at a time per message")
+ expect(guidelines).not.toContain("After each tool use, the user will respond")
})
- describe("with MULTIPLE_NATIVE_TOOL_CALLS enabled", () => {
- it("should include multiple-tools-per-message guidance when experiment enabled", () => {
- const guidelines = getToolUseGuidelinesSection(TOOL_PROTOCOL.NATIVE, {
- [EXPERIMENT_IDS.MULTIPLE_NATIVE_TOOL_CALLS]: true,
- })
-
- expect(guidelines).toContain("you may use multiple tools in a single message")
- expect(guidelines).not.toContain("use one tool at a time per message")
+ it("should use simplified footer without step-by-step language", () => {
+ const guidelines = getToolUseGuidelinesSection({
+ [EXPERIMENT_IDS.MULTIPLE_NATIVE_TOOL_CALLS]: true,
})
- it("should include simplified iterative process guidelines", () => {
- const guidelines = getToolUseGuidelinesSection(TOOL_PROTOCOL.NATIVE, {
- [EXPERIMENT_IDS.MULTIPLE_NATIVE_TOOL_CALLS]: true,
- })
-
- expect(guidelines).toContain("carefully considering the user's response after tool executions")
- expect(guidelines).not.toContain("It is crucial to proceed step-by-step")
- })
+ // When multiple tools per message is enabled, we don't want the
+ // "step-by-step" or "after each tool use" language that would
+ // contradict the ability to batch tool calls.
+ expect(guidelines).toContain("carefully considering the user's response after tool executions")
+ expect(guidelines).not.toContain("It is crucial to proceed step-by-step")
+ expect(guidelines).not.toContain("ALWAYS wait for user confirmation after each tool use")
})
})
- it("should include common guidance regardless of protocol", () => {
- const guidelinesXml = getToolUseGuidelinesSection(TOOL_PROTOCOL.XML)
- const guidelinesNative = getToolUseGuidelinesSection(TOOL_PROTOCOL.NATIVE)
-
- for (const guidelines of [guidelinesXml, guidelinesNative]) {
- expect(guidelines).toContain("Assess what information you already have")
- expect(guidelines).toContain("Choose the most appropriate tool")
- expect(guidelines).toContain("After each tool use, the user will respond")
- }
+ it("should include common guidance", () => {
+ const guidelines = getToolUseGuidelinesSection()
+ expect(guidelines).toContain("Assess what information you already have")
+ expect(guidelines).toContain("Choose the most appropriate tool")
+ expect(guidelines).toContain("After each tool use, the user will respond")
+ // No legacy XML-tag tool-calling remnants
+ expect(guidelines).not.toContain("")
})
})
diff --git a/src/core/prompts/sections/__tests__/tool-use.spec.ts b/src/core/prompts/sections/__tests__/tool-use.spec.ts
index c8e3a9b5d0..1d945612ea 100644
--- a/src/core/prompts/sections/__tests__/tool-use.spec.ts
+++ b/src/core/prompts/sections/__tests__/tool-use.spec.ts
@@ -1,41 +1,24 @@
import { getSharedToolUseSection } from "../tool-use"
-import { TOOL_PROTOCOL } from "@roo-code/types"
describe("getSharedToolUseSection", () => {
- describe("XML protocol", () => {
- it("should include one tool per message requirement", () => {
- const section = getSharedToolUseSection(TOOL_PROTOCOL.XML)
-
- expect(section).toContain("You must use exactly one tool per message")
- expect(section).toContain("every assistant message must include a tool call")
- })
-
- it("should include XML formatting instructions", () => {
- const section = getSharedToolUseSection(TOOL_PROTOCOL.XML)
-
- expect(section).toContain("XML-style tags")
- expect(section).toContain("Always use the actual tool name as the XML tag name")
- })
- })
-
- describe("native protocol", () => {
+ describe("native tool calling", () => {
it("should include one tool per message requirement when experiment is disabled", () => {
// No experiment flags passed (default: disabled)
- const section = getSharedToolUseSection(TOOL_PROTOCOL.NATIVE)
+ const section = getSharedToolUseSection()
expect(section).toContain("You must use exactly one tool call per assistant response")
expect(section).toContain("Do not call zero tools or more than one tool")
})
it("should include one tool per message requirement when experiment is explicitly disabled", () => {
- const section = getSharedToolUseSection(TOOL_PROTOCOL.NATIVE, { multipleNativeToolCalls: false })
+ const section = getSharedToolUseSection({ multipleNativeToolCalls: false })
expect(section).toContain("You must use exactly one tool call per assistant response")
expect(section).toContain("Do not call zero tools or more than one tool")
})
it("should NOT include one tool per message requirement when experiment is enabled", () => {
- const section = getSharedToolUseSection(TOOL_PROTOCOL.NATIVE, { multipleNativeToolCalls: true })
+ const section = getSharedToolUseSection({ multipleNativeToolCalls: true })
expect(section).not.toContain("You must use exactly one tool per message")
expect(section).not.toContain("every assistant message must include a tool call")
@@ -44,26 +27,26 @@ describe("getSharedToolUseSection", () => {
})
it("should include native tool-calling instructions", () => {
- const section = getSharedToolUseSection(TOOL_PROTOCOL.NATIVE)
+ const section = getSharedToolUseSection()
expect(section).toContain("provider-native tool-calling mechanism")
expect(section).toContain("Do not include XML markup or examples")
})
it("should NOT include XML formatting instructions", () => {
- const section = getSharedToolUseSection(TOOL_PROTOCOL.NATIVE)
+ const section = getSharedToolUseSection()
- expect(section).not.toContain("XML-style tags")
- expect(section).not.toContain("Always use the actual tool name as the XML tag name")
+ expect(section).not.toContain("")
+ expect(section).not.toContain("")
})
})
- describe("default protocol", () => {
- it("should default to XML protocol when no protocol is specified", () => {
+ describe("default (native-only)", () => {
+ it("should default to native tool calling when no arguments are provided", () => {
const section = getSharedToolUseSection()
-
- expect(section).toContain("XML-style tags")
- expect(section).toContain("You must use exactly one tool per message")
+ expect(section).toContain("provider-native tool-calling mechanism")
+ // No legacy XML-tag tool-calling remnants
+ expect(section).not.toContain("")
})
})
})
diff --git a/src/core/prompts/sections/custom-instructions.ts b/src/core/prompts/sections/custom-instructions.ts
index ed33f4a1e3..8eee0a0998 100644
--- a/src/core/prompts/sections/custom-instructions.ts
+++ b/src/core/prompts/sections/custom-instructions.ts
@@ -6,7 +6,6 @@ import { Dirent } from "fs"
import { isLanguage } from "@roo-code/types"
import type { SystemPromptSettings } from "../types"
-import { getEffectiveProtocol, isNativeProtocol } from "@roo-code/types"
import { LANGUAGES } from "../../../shared/language"
import {
@@ -459,17 +458,13 @@ export async function addCustomInstructions(
const joinedSections = sections.join("\n\n")
- const effectiveProtocol = getEffectiveProtocol(options.settings?.toolProtocol)
-
return joinedSections
? `
====
USER'S CUSTOM INSTRUCTIONS
-The following additional instructions are provided by the user, and should be followed to the best of your ability${
- isNativeProtocol(effectiveProtocol) ? "." : " 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.
${joinedSections}
`
diff --git a/src/core/prompts/sections/mcp-servers.ts b/src/core/prompts/sections/mcp-servers.ts
index 3eb1569c5a..42a6d5d440 100644
--- a/src/core/prompts/sections/mcp-servers.ts
+++ b/src/core/prompts/sections/mcp-servers.ts
@@ -17,7 +17,6 @@ export async function getMcpServersSection(
.getServers()
.filter((server) => server.status === "connected")
.map((server) => {
- // Only include tool descriptions when using XML protocol
const tools = includeToolDescriptions
? server.tools
?.filter((tool) => tool.enabledForPrompt !== false)
@@ -56,7 +55,7 @@ export async function getMcpServersSection(
// Different instructions based on protocol
const toolAccessInstructions = includeToolDescriptions
? `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.`
- : `When a server is connected, each server's tools are available as native tools with the naming pattern \`mcp_{server_name}_{tool_name}\`. For example, a tool named 'get_forecast' from a server named 'weather' would be available as \`mcp_weather_get_forecast\`. You can also access server resources using the \`access_mcp_resource\` tool.`
+ : `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.`
const baseSection = `MCP SERVERS
diff --git a/src/core/prompts/sections/rules.ts b/src/core/prompts/sections/rules.ts
index 800fb430ef..4f6e573fa7 100644
--- a/src/core/prompts/sections/rules.ts
+++ b/src/core/prompts/sections/rules.ts
@@ -1,5 +1,4 @@
import type { SystemPromptSettings } from "../types"
-import { getEffectiveProtocol, isNativeProtocol } from "@roo-code/types"
import { getShell } from "../../../utils/shell"
@@ -64,9 +63,6 @@ When asked about your creator, vendor, or company, respond with:
}
export function getRulesSection(cwd: string, settings?: SystemPromptSettings): string {
- // Determine whether to use XML tool references based on protocol
- const effectiveProtocol = getEffectiveProtocol(settings?.toolProtocol)
-
// Get shell-appropriate command chaining operator
const chainOp = getCommandChainOperator()
const chainNote = getCommandChainNote()
@@ -76,7 +72,7 @@ export function getRulesSection(cwd: string, settings?: SystemPromptSettings): s
RULES
- The project base directory is: ${cwd.toPosix()}
-- 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 ${isNativeProtocol(effectiveProtocol) ? "execute_command" : ""}.
+- 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 '${cwd.toPosix()}', 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 '${cwd.toPosix()}', and if so prepend with \`cd\`'ing into that directory ${chainOp} then executing the command (as one command since you are stuck operating from '${cwd.toPosix()}'). For example, if you needed to run \`npm install\` in a project outside of '${cwd.toPosix()}', you would need to prepend with a \`cd\` i.e. pseudocode for this would be \`cd (path to project) ${chainOp} (command, in this case npm install)\`.${chainNote ? ` ${chainNote}` : ""}
diff --git a/src/core/prompts/sections/skills.ts b/src/core/prompts/sections/skills.ts
index 954d0451bd..53ba8b95f1 100644
--- a/src/core/prompts/sections/skills.ts
+++ b/src/core/prompts/sections/skills.ts
@@ -80,6 +80,16 @@ CONSTRAINTS:
- FAILURE to perform this check is an error.
+
+- When a SKILL.md is loaded, ONLY the contents of SKILL.md are present.
+- Files linked from SKILL.md are NOT loaded automatically.
+- The model MUST explicitly decide to read a linked file based on task relevance.
+- Do NOT assume the contents of linked files unless they have been explicitly read.
+- Prefer reading the minimum necessary linked file.
+- Avoid reading multiple linked files unless required.
+- Treat linked files as progressive disclosure, not mandatory context.
+
+
- The skill list is already filtered for the current mode: "${currentMode}".
- Mode-specific skills may come from skills-${currentMode}/ with project-level overrides taking precedence over global skills.
diff --git a/src/core/prompts/sections/tool-use-guidelines.ts b/src/core/prompts/sections/tool-use-guidelines.ts
index a5dad2cc0b..256e7aba5a 100644
--- a/src/core/prompts/sections/tool-use-guidelines.ts
+++ b/src/core/prompts/sections/tool-use-guidelines.ts
@@ -1,12 +1,6 @@
-import { ToolProtocol, TOOL_PROTOCOL } from "@roo-code/types"
-import { isNativeProtocol } from "@roo-code/types"
-
import { experiments, EXPERIMENT_IDS } from "../../../shared/experiments"
-export function getToolUseGuidelinesSection(
- protocol: ToolProtocol = TOOL_PROTOCOL.XML,
- experimentFlags?: Record,
-): string {
+export function getToolUseGuidelinesSection(experimentFlags?: Record): string {
// Build guidelines array with automatic numbering
let itemNumber = 1
const guidelinesList: string[] = []
@@ -20,50 +14,43 @@ export function getToolUseGuidelinesSection(
`${itemNumber++}. 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.`,
)
- // Remaining guidelines - different for native vs XML protocol
- if (isNativeProtocol(protocol)) {
- // Check if multiple native tool calls is enabled via experiment
- const isMultipleNativeToolCallsEnabled = experiments.isEnabled(
- experimentFlags ?? {},
- EXPERIMENT_IDS.MULTIPLE_NATIVE_TOOL_CALLS,
- )
+ // Native-only guidelines.
+ // Check if multiple native tool calls is enabled via experiment.
+ const isMultipleNativeToolCallsEnabled = experiments.isEnabled(
+ experimentFlags ?? {},
+ EXPERIMENT_IDS.MULTIPLE_NATIVE_TOOL_CALLS,
+ )
- if (isMultipleNativeToolCallsEnabled) {
- guidelinesList.push(
- `${itemNumber++}. 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.`,
- )
- } else {
- guidelinesList.push(
- `${itemNumber++}. 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.`,
- )
- }
+ if (isMultipleNativeToolCallsEnabled) {
+ guidelinesList.push(
+ `${itemNumber++}. 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.`,
+ )
} else {
guidelinesList.push(
`${itemNumber++}. 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.`,
)
}
-
- // Protocol-specific guideline - only add for XML protocol
- if (!isNativeProtocol(protocol)) {
- guidelinesList.push(`${itemNumber++}. Formulate your tool use using the XML format specified for each tool.`)
- }
- guidelinesList.push(`${itemNumber++}. 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:
+ // Only add the per-tool confirmation guideline when NOT using multiple tool calls.
+ // When multiple tool calls are enabled, results may arrive batched (after all tools),
+ // so "after each tool use" would contradict the batching behavior.
+ if (!isMultipleNativeToolCallsEnabled) {
+ guidelinesList.push(`${itemNumber++}. 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.`)
+ }
- // Only add the "wait for confirmation" guideline for XML protocol
- // Native protocol allows multiple tools per message, so waiting after each tool doesn't apply
- if (!isNativeProtocol(protocol)) {
+ // Only add the "wait for confirmation" guideline when NOT using multiple tool calls.
+ // With multiple tool calls enabled, the model is expected to batch tools and get results together.
+ if (!isMultipleNativeToolCallsEnabled) {
guidelinesList.push(
`${itemNumber++}. 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.`,
)
}
// Join guidelines and add the footer
- // For native protocol, the footer is less relevant since multiple tools can execute in one message
- const footer = isNativeProtocol(protocol)
+ const footer = isMultipleNativeToolCallsEnabled
? `\n\nBy 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.`
: `\n\nIt 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.
diff --git a/src/core/prompts/sections/tool-use.ts b/src/core/prompts/sections/tool-use.ts
index c3f5e221b8..4bc0905c56 100644
--- a/src/core/prompts/sections/tool-use.ts
+++ b/src/core/prompts/sections/tool-use.ts
@@ -1,44 +1,19 @@
-import { ToolProtocol, TOOL_PROTOCOL, isNativeProtocol } from "@roo-code/types"
-
import { experiments, EXPERIMENT_IDS } from "../../../shared/experiments"
-export function getSharedToolUseSection(
- protocol: ToolProtocol = TOOL_PROTOCOL.XML,
- experimentFlags?: Record,
-): string {
- if (isNativeProtocol(protocol)) {
- // Check if multiple native tool calls is enabled via experiment
- const isMultipleNativeToolCallsEnabled = experiments.isEnabled(
- experimentFlags ?? {},
- EXPERIMENT_IDS.MULTIPLE_NATIVE_TOOL_CALLS,
- )
+export function getSharedToolUseSection(experimentFlags?: Record): string {
+ // Check if multiple native tool calls is enabled via experiment
+ const isMultipleNativeToolCallsEnabled = experiments.isEnabled(
+ experimentFlags ?? {},
+ EXPERIMENT_IDS.MULTIPLE_NATIVE_TOOL_CALLS,
+ )
- const toolUseGuidance = isMultipleNativeToolCallsEnabled
- ? " 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."
- : " You must use exactly one tool call per assistant response. Do not call zero tools or more than one tool in the same response."
-
- return `====
-
-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.${toolUseGuidance}`
- }
+ const toolUseGuidance = isMultipleNativeToolCallsEnabled
+ ? " 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."
+ : " You must use exactly one tool call per assistant response. Do not call zero tools or more than one tool in the same response."
return `====
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.`
+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.${toolUseGuidance}`
}
diff --git a/src/core/prompts/system.ts b/src/core/prompts/system.ts
index 040d703929..013cf28452 100644
--- a/src/core/prompts/system.ts
+++ b/src/core/prompts/system.ts
@@ -1,15 +1,7 @@
import * as vscode from "vscode"
import * as os from "os"
-import {
- type ModeConfig,
- type PromptComponent,
- type CustomModePrompts,
- type TodoItem,
- getEffectiveProtocol,
- isNativeProtocol,
-} from "@roo-code/types"
-import { customToolRegistry, formatXml } from "@roo-code/core"
+import { type ModeConfig, type PromptComponent, type CustomModePrompts, type TodoItem } from "@roo-code/types"
import { Mode, modes, defaultModeSlug, getModeBySlug, getGroupName, getModeSelection } from "../../shared/modes"
import { DiffStrategy } from "../../shared/tools"
@@ -23,7 +15,6 @@ import { SkillsManager } from "../../services/skills/SkillsManager"
import { PromptVariables, loadSystemPromptFile } from "./sections/custom-system-prompt"
import type { SystemPromptSettings } from "./types"
-import { getToolDescriptionsForMode } from "./tools"
import {
getRulesSection,
getSystemInfoSection,
@@ -62,7 +53,6 @@ async function generatePrompt(
promptComponent?: PromptComponent,
customModeConfigs?: ModeConfig[],
globalCustomInstructions?: string,
- diffEnabled?: boolean,
experiments?: Record,
enableMcpServerCreation?: boolean,
language?: string,
@@ -77,9 +67,6 @@ async function generatePrompt(
throw new Error("Extension context is required for generating system prompt")
}
- // If diff is disabled, don't pass the diffStrategy
- const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined
-
// Get the full mode config to ensure we have the role definition (used for groups, etc.)
const modeConfig = getModeBySlug(mode, customModeConfigs) || modes.find((m) => m.slug === mode) || modes[0]
const { roleDefinition, baseInstructions } = getModeSelection(mode, promptComponent, customModeConfigs)
@@ -91,60 +78,24 @@ async function generatePrompt(
const codeIndexManager = CodeIndexManager.getInstance(context, cwd)
- // Determine the effective protocol (defaults to 'xml')
- const effectiveProtocol = getEffectiveProtocol(settings?.toolProtocol)
-
const [modesSection, mcpServersSection, skillsSection] = await Promise.all([
getModesSection(context),
shouldIncludeMcp
- ? getMcpServersSection(
- mcpHub,
- effectiveDiffStrategy,
- enableMcpServerCreation,
- !isNativeProtocol(effectiveProtocol),
- )
+ ? getMcpServersSection(mcpHub, diffStrategy, enableMcpServerCreation, false)
: Promise.resolve(""),
getSkillsSection(skillsManager, mode as string),
])
- // Build tools catalog section only for XML protocol
- const builtInToolsCatalog = isNativeProtocol(effectiveProtocol)
- ? ""
- : `\n\n${getToolDescriptionsForMode(
- mode,
- cwd,
- supportsComputerUse,
- codeIndexManager,
- effectiveDiffStrategy,
- browserViewportSize,
- shouldIncludeMcp ? mcpHub : undefined,
- customModeConfigs,
- experiments,
- partialReadsEnabled,
- settings,
- enableMcpServerCreation,
- modelId,
- )}`
-
- let customToolsSection = ""
-
- if (experiments?.customTools && !isNativeProtocol(effectiveProtocol)) {
- const customTools = customToolRegistry.getAllSerialized()
-
- if (customTools.length > 0) {
- customToolsSection = `\n\n${formatXml(customTools)}`
- }
- }
-
- const toolsCatalog = builtInToolsCatalog + customToolsSection
+ // Tools catalog is not included in the system prompt.
+ const toolsCatalog = ""
const basePrompt = `${roleDefinition}
${markdownFormattingSection()}
-${getSharedToolUseSection(effectiveProtocol, experiments)}${toolsCatalog}
+${getSharedToolUseSection(experiments)}${toolsCatalog}
-${getToolUseGuidelinesSection(effectiveProtocol, experiments)}
+ ${getToolUseGuidelinesSection(experiments)}
${mcpServersSection}
@@ -178,7 +129,6 @@ export const SYSTEM_PROMPT = async (
customModePrompts?: CustomModePrompts,
customModes?: ModeConfig[],
globalCustomInstructions?: string,
- diffEnabled?: boolean,
experiments?: Record,
enableMcpServerCreation?: boolean,
language?: string,
@@ -237,21 +187,17 @@ ${fileCustomSystemPrompt}
${customInstructions}`
}
- // If diff is disabled, don't pass the diffStrategy
- const effectiveDiffStrategy = diffEnabled ? diffStrategy : undefined
-
return generatePrompt(
context,
cwd,
supportsComputerUse,
currentMode.slug,
mcpHub,
- effectiveDiffStrategy,
+ diffStrategy,
browserViewportSize,
promptComponent,
customModes,
globalCustomInstructions,
- diffEnabled,
experiments,
enableMcpServerCreation,
language,
diff --git a/src/core/prompts/tools/__tests__/access-mcp-resource.spec.ts b/src/core/prompts/tools/__tests__/access-mcp-resource.spec.ts
deleted file mode 100644
index 1a927937ff..0000000000
--- a/src/core/prompts/tools/__tests__/access-mcp-resource.spec.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import { getAccessMcpResourceDescription } from "../access-mcp-resource"
-import { ToolArgs } from "../types"
-import { McpHub } from "../../../../services/mcp/McpHub"
-
-describe("getAccessMcpResourceDescription", () => {
- const baseArgs: Omit = {
- cwd: "/test",
- supportsComputerUse: false,
- }
-
- it("should return undefined when mcpHub is not provided", () => {
- const args: ToolArgs = {
- ...baseArgs,
- mcpHub: undefined,
- }
-
- const result = getAccessMcpResourceDescription(args)
- expect(result).toBeUndefined()
- })
-
- it("should return undefined when mcpHub has no servers with resources", () => {
- const mockMcpHub = {
- getServers: () => [
- {
- name: "test-server",
- resources: [],
- },
- ],
- } as unknown as McpHub
-
- const args: ToolArgs = {
- ...baseArgs,
- mcpHub: mockMcpHub,
- }
-
- const result = getAccessMcpResourceDescription(args)
- expect(result).toBeUndefined()
- })
-
- it("should return undefined when mcpHub has servers with undefined resources", () => {
- const mockMcpHub = {
- getServers: () => [
- {
- name: "test-server",
- resources: undefined,
- },
- ],
- } as unknown as McpHub
-
- const args: ToolArgs = {
- ...baseArgs,
- mcpHub: mockMcpHub,
- }
-
- const result = getAccessMcpResourceDescription(args)
- expect(result).toBeUndefined()
- })
-
- it("should return undefined when mcpHub has no servers", () => {
- const mockMcpHub = {
- getServers: () => [],
- } as unknown as McpHub
-
- const args: ToolArgs = {
- ...baseArgs,
- mcpHub: mockMcpHub,
- }
-
- const result = getAccessMcpResourceDescription(args)
- expect(result).toBeUndefined()
- })
-
- it("should return description when mcpHub has servers with resources", () => {
- const mockMcpHub = {
- getServers: () => [
- {
- name: "test-server",
- resources: [{ uri: "test://resource", name: "Test Resource" }],
- },
- ],
- } as unknown as McpHub
-
- const args: ToolArgs = {
- ...baseArgs,
- mcpHub: mockMcpHub,
- }
-
- const result = getAccessMcpResourceDescription(args)
- expect(result).toBeDefined()
- expect(result).toContain("## access_mcp_resource")
- expect(result).toContain("server_name")
- expect(result).toContain("uri")
- })
-
- it("should return description when at least one server has resources", () => {
- const mockMcpHub = {
- getServers: () => [
- {
- name: "server-without-resources",
- resources: [],
- },
- {
- name: "server-with-resources",
- resources: [{ uri: "test://resource", name: "Test Resource" }],
- },
- ],
- } as unknown as McpHub
-
- const args: ToolArgs = {
- ...baseArgs,
- mcpHub: mockMcpHub,
- }
-
- const result = getAccessMcpResourceDescription(args)
- expect(result).toBeDefined()
- expect(result).toContain("## access_mcp_resource")
- })
-})
diff --git a/src/core/prompts/tools/__tests__/attempt-completion.spec.ts b/src/core/prompts/tools/__tests__/attempt-completion.spec.ts
deleted file mode 100644
index 026d73789f..0000000000
--- a/src/core/prompts/tools/__tests__/attempt-completion.spec.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { getAttemptCompletionDescription } from "../attempt-completion"
-
-describe("getAttemptCompletionDescription", () => {
- it("should NOT include command parameter in the description", () => {
- const args = {
- cwd: "/test/path",
- supportsComputerUse: false,
- }
-
- const description = getAttemptCompletionDescription(args)
-
- // Check that command parameter is NOT included (permanently disabled)
- expect(description).not.toContain("- command: (optional)")
- expect(description).not.toContain("A CLI command to execute to show a live demo")
- expect(description).not.toContain("Command to demonstrate result (optional)")
- expect(description).not.toContain("open index.html")
-
- // But should still have the basic structure
- expect(description).toContain("## attempt_completion")
- expect(description).toContain("- result: (required)")
- expect(description).toContain("")
- expect(description).toContain("")
- })
-
- it("should work when no args provided", () => {
- const description = getAttemptCompletionDescription()
-
- // Check that command parameter is NOT included (permanently disabled)
- expect(description).not.toContain("- command: (optional)")
- expect(description).not.toContain("A CLI command to execute to show a live demo")
- expect(description).not.toContain("Command to demonstrate result (optional)")
- expect(description).not.toContain("open index.html")
-
- // But should still have the basic structure
- expect(description).toContain("## attempt_completion")
- expect(description).toContain("- result: (required)")
- expect(description).toContain("")
- expect(description).toContain("")
- })
-
- it("should show example without command", () => {
- const args = {
- cwd: "/test/path",
- supportsComputerUse: false,
- }
-
- const description = getAttemptCompletionDescription(args)
-
- // Check example format
- expect(description).toContain("Example: Requesting to attempt completion with a result")
- expect(description).toContain("I've updated the CSS")
- expect(description).not.toContain("Example: Requesting to attempt completion with a result and command")
- })
-
- it("should contain core functionality description", () => {
- const description = getAttemptCompletionDescription()
-
- // Should contain core functionality
- const coreText = "After each tool use, the user will respond with the result of that tool use"
- expect(description).toContain(coreText)
-
- // Should contain the important note
- const importantNote = "IMPORTANT NOTE: This tool CANNOT be used until you've confirmed"
- expect(description).toContain(importantNote)
-
- // Should contain result parameter
- expect(description).toContain("- result: (required)")
- })
-})
diff --git a/src/core/prompts/tools/__tests__/fetch-instructions.spec.ts b/src/core/prompts/tools/__tests__/fetch-instructions.spec.ts
deleted file mode 100644
index ef01f132f5..0000000000
--- a/src/core/prompts/tools/__tests__/fetch-instructions.spec.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { getFetchInstructionsDescription } from "../fetch-instructions"
-
-describe("getFetchInstructionsDescription", () => {
- it("should include create_mcp_server when enableMcpServerCreation is true", () => {
- const description = getFetchInstructionsDescription(true)
-
- expect(description).toContain("create_mcp_server")
- expect(description).toContain("create_mode")
- expect(description).toContain("Example: Requesting instructions to create an MCP Server")
- expect(description).toContain("create_mcp_server")
- })
-
- it("should include create_mcp_server when enableMcpServerCreation is undefined (default behavior)", () => {
- const description = getFetchInstructionsDescription()
-
- expect(description).toContain("create_mcp_server")
- expect(description).toContain("create_mode")
- expect(description).toContain("Example: Requesting instructions to create an MCP Server")
- expect(description).toContain("create_mcp_server")
- })
-
- it("should exclude create_mcp_server when enableMcpServerCreation is false", () => {
- const description = getFetchInstructionsDescription(false)
-
- expect(description).not.toContain("create_mcp_server")
- expect(description).toContain("create_mode")
- expect(description).toContain("Example: Requesting instructions to create a Mode")
- expect(description).toContain("create_mode")
- expect(description).not.toContain("Example: Requesting instructions to create an MCP Server")
- })
-
- it("should have the correct structure", () => {
- const description = getFetchInstructionsDescription(true)
-
- expect(description).toContain("## fetch_instructions")
- expect(description).toContain("Description: Request to fetch instructions to perform a task")
- expect(description).toContain("Parameters:")
- expect(description).toContain("- task: (required) The task to get instructions for.")
- expect(description).toContain("")
- expect(description).toContain("")
- })
-
- it("should handle null value consistently (treat as default/undefined)", () => {
- const description = getFetchInstructionsDescription(null as any)
-
- // Should behave the same as undefined (default to true)
- expect(description).toContain("create_mcp_server")
- expect(description).toContain("create_mode")
- expect(description).toContain("Example: Requesting instructions to create an MCP Server")
- expect(description).toContain("create_mcp_server")
- })
-})
diff --git a/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts b/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts
deleted file mode 100644
index 5cdfe2f1e7..0000000000
--- a/src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts
+++ /dev/null
@@ -1,912 +0,0 @@
-import { describe, it, expect, beforeEach, afterEach } from "vitest"
-import type OpenAI from "openai"
-import type { ModeConfig, ModelInfo } from "@roo-code/types"
-import {
- filterNativeToolsForMode,
- filterMcpToolsForMode,
- applyModelToolCustomization,
- resolveToolAlias,
-} from "../filter-tools-for-mode"
-import * as toolsModule from "../../../../shared/tools"
-
-describe("filterNativeToolsForMode", () => {
- const mockNativeTools: OpenAI.Chat.ChatCompletionTool[] = [
- {
- type: "function",
- function: {
- name: "read_file",
- description: "Read files",
- parameters: {},
- },
- },
- {
- type: "function",
- function: {
- name: "write_to_file",
- description: "Write files",
- parameters: {},
- },
- },
- {
- type: "function",
- function: {
- name: "apply_diff",
- description: "Apply diff",
- parameters: {},
- },
- },
- {
- type: "function",
- function: {
- name: "execute_command",
- description: "Execute command",
- parameters: {},
- },
- },
- {
- type: "function",
- function: {
- name: "browser_action",
- description: "Browser action",
- parameters: {},
- },
- },
- {
- type: "function",
- function: {
- name: "ask_followup_question",
- description: "Ask question",
- parameters: {},
- },
- },
- {
- type: "function",
- function: {
- name: "attempt_completion",
- description: "Complete task",
- parameters: {},
- },
- },
- ]
-
- it("should filter tools for architect mode (read, browser, mcp only)", () => {
- const architectMode: ModeConfig = {
- slug: "architect",
- name: "Architect",
- roleDefinition: "Test",
- groups: ["read", "browser", "mcp"] as const,
- }
-
- const filtered = filterNativeToolsForMode(
- mockNativeTools,
- "architect",
- [architectMode],
- {},
- undefined,
- {},
- undefined,
- )
-
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
-
- // Should include read tools
- expect(toolNames).toContain("read_file")
-
- // Should NOT include edit tools
- expect(toolNames).not.toContain("write_to_file")
- expect(toolNames).not.toContain("apply_diff")
-
- // Should NOT include command tools
- expect(toolNames).not.toContain("execute_command")
-
- // Should include browser tools
- expect(toolNames).toContain("browser_action")
-
- // Should ALWAYS include always-available tools
- expect(toolNames).toContain("ask_followup_question")
- expect(toolNames).toContain("attempt_completion")
- })
-
- it("should filter tools for code mode (all groups)", () => {
- const codeMode: ModeConfig = {
- slug: "code",
- name: "Code",
- roleDefinition: "Test",
- groups: ["read", "edit", "browser", "command", "mcp"] as const,
- }
-
- const filtered = filterNativeToolsForMode(mockNativeTools, "code", [codeMode], {}, undefined, {}, undefined)
-
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
-
- // Should include all tools (code mode has all groups)
- expect(toolNames).toContain("read_file")
- expect(toolNames).toContain("write_to_file")
- expect(toolNames).toContain("apply_diff")
- expect(toolNames).toContain("execute_command")
- expect(toolNames).toContain("browser_action")
- expect(toolNames).toContain("ask_followup_question")
- expect(toolNames).toContain("attempt_completion")
- })
-
- it("should always include always-available tools regardless of mode groups", () => {
- const restrictiveMode: ModeConfig = {
- slug: "restrictive",
- name: "Restrictive",
- roleDefinition: "Test",
- groups: [] as const, // No groups
- }
-
- const filtered = filterNativeToolsForMode(
- mockNativeTools,
- "restrictive",
- [restrictiveMode],
- {},
- undefined,
- {},
- undefined,
- )
-
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
-
- // Should still include always-available tools
- expect(toolNames).toContain("ask_followup_question")
- expect(toolNames).toContain("attempt_completion")
-
- // Should NOT include any other tools
- expect(toolNames).not.toContain("read_file")
- expect(toolNames).not.toContain("write_to_file")
- expect(toolNames).not.toContain("execute_command")
- })
-
- it("should handle undefined mode by using default mode", () => {
- const filtered = filterNativeToolsForMode(mockNativeTools, undefined, undefined, {}, undefined, {}, undefined)
-
- // Should return some tools (default mode is code which has all groups)
- expect(filtered.length).toBeGreaterThan(0)
-
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
- expect(toolNames).toContain("ask_followup_question")
- expect(toolNames).toContain("attempt_completion")
- })
-
- it("should exclude codebase_search when codeIndexManager is not configured", () => {
- const codeMode: ModeConfig = {
- slug: "code",
- name: "Code",
- roleDefinition: "Test",
- groups: ["read", "edit", "browser", "command", "mcp"] as const,
- }
-
- const mockCodebaseSearchTool: OpenAI.Chat.ChatCompletionTool = {
- type: "function",
- function: {
- name: "codebase_search",
- description: "Search codebase",
- parameters: {},
- },
- }
-
- const toolsWithCodebaseSearch = [...mockNativeTools, mockCodebaseSearchTool]
-
- // Without codeIndexManager
- const filtered = filterNativeToolsForMode(
- toolsWithCodebaseSearch,
- "code",
- [codeMode],
- {},
- undefined,
- {},
- undefined,
- )
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
- expect(toolNames).not.toContain("codebase_search")
- })
-
- it("should exclude access_mcp_resource when mcpHub is not provided", () => {
- const codeMode: ModeConfig = {
- slug: "code",
- name: "Code",
- roleDefinition: "Test",
- groups: ["read", "edit", "browser", "command", "mcp"] as const,
- }
-
- const mockAccessMcpResourceTool: OpenAI.Chat.ChatCompletionTool = {
- type: "function",
- function: {
- name: "access_mcp_resource",
- description: "Access MCP resource",
- parameters: {},
- },
- }
-
- const toolsWithAccessMcpResource = [...mockNativeTools, mockAccessMcpResourceTool]
-
- // Without mcpHub
- const filtered = filterNativeToolsForMode(
- toolsWithAccessMcpResource,
- "code",
- [codeMode],
- {},
- undefined,
- {},
- undefined,
- )
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
- expect(toolNames).not.toContain("access_mcp_resource")
- })
-
- it("should exclude access_mcp_resource when mcpHub has no resources", () => {
- const codeMode: ModeConfig = {
- slug: "code",
- name: "Code",
- roleDefinition: "Test",
- groups: ["read", "edit", "browser", "command", "mcp"] as const,
- }
-
- const mockAccessMcpResourceTool: OpenAI.Chat.ChatCompletionTool = {
- type: "function",
- function: {
- name: "access_mcp_resource",
- description: "Access MCP resource",
- parameters: {},
- },
- }
-
- const toolsWithAccessMcpResource = [...mockNativeTools, mockAccessMcpResourceTool]
-
- // Mock mcpHub with no resources
- const mockMcpHub = {
- getServers: () => [
- {
- name: "test-server",
- resources: [],
- },
- ],
- } as any
-
- const filtered = filterNativeToolsForMode(
- toolsWithAccessMcpResource,
- "code",
- [codeMode],
- {},
- undefined,
- {},
- mockMcpHub,
- )
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
- expect(toolNames).not.toContain("access_mcp_resource")
- })
-
- it("should include access_mcp_resource when mcpHub has resources", () => {
- const codeMode: ModeConfig = {
- slug: "code",
- name: "Code",
- roleDefinition: "Test",
- groups: ["read", "edit", "browser", "command", "mcp"] as const,
- }
-
- const mockAccessMcpResourceTool: OpenAI.Chat.ChatCompletionTool = {
- type: "function",
- function: {
- name: "access_mcp_resource",
- description: "Access MCP resource",
- parameters: {},
- },
- }
-
- const toolsWithAccessMcpResource = [...mockNativeTools, mockAccessMcpResourceTool]
-
- // Mock mcpHub with resources
- const mockMcpHub = {
- getServers: () => [
- {
- name: "test-server",
- resources: [{ uri: "test://resource", name: "Test Resource" }],
- },
- ],
- } as any
-
- const filtered = filterNativeToolsForMode(
- toolsWithAccessMcpResource,
- "code",
- [codeMode],
- {},
- undefined,
- {},
- mockMcpHub,
- )
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
- expect(toolNames).toContain("access_mcp_resource")
- })
-
- it("should exclude update_todo_list when todoListEnabled is false", () => {
- const codeMode: ModeConfig = {
- slug: "code",
- name: "Code",
- roleDefinition: "Test",
- groups: ["read", "edit", "browser", "command", "mcp"] as const,
- }
-
- const mockTodoTool: OpenAI.Chat.ChatCompletionTool = {
- type: "function",
- function: {
- name: "update_todo_list",
- description: "Update todo list",
- parameters: {},
- },
- }
-
- const toolsWithTodo = [...mockNativeTools, mockTodoTool]
-
- const filtered = filterNativeToolsForMode(
- toolsWithTodo,
- "code",
- [codeMode],
- {},
- undefined,
- {
- todoListEnabled: false,
- },
- undefined,
- )
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
- expect(toolNames).not.toContain("update_todo_list")
- })
-
- it("should exclude generate_image when experiment is not enabled", () => {
- const codeMode: ModeConfig = {
- slug: "code",
- name: "Code",
- roleDefinition: "Test",
- groups: ["read", "edit", "browser", "command", "mcp"] as const,
- }
-
- const mockImageTool: OpenAI.Chat.ChatCompletionTool = {
- type: "function",
- function: {
- name: "generate_image",
- description: "Generate image",
- parameters: {},
- },
- }
-
- const toolsWithImage = [...mockNativeTools, mockImageTool]
-
- const filtered = filterNativeToolsForMode(
- toolsWithImage,
- "code",
- [codeMode],
- { imageGeneration: false },
- undefined,
- {},
- undefined,
- )
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
- expect(toolNames).not.toContain("generate_image")
- })
-
- it("should exclude run_slash_command when experiment is not enabled", () => {
- const codeMode: ModeConfig = {
- slug: "code",
- name: "Code",
- roleDefinition: "Test",
- groups: ["read", "edit", "browser", "command", "mcp"] as const,
- }
-
- const mockSlashCommandTool: OpenAI.Chat.ChatCompletionTool = {
- type: "function",
- function: {
- name: "run_slash_command",
- description: "Run slash command",
- parameters: {},
- },
- }
-
- const toolsWithSlashCommand = [...mockNativeTools, mockSlashCommandTool]
-
- const filtered = filterNativeToolsForMode(
- toolsWithSlashCommand,
- "code",
- [codeMode],
- { runSlashCommand: false },
- undefined,
- {},
- undefined,
- )
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
- expect(toolNames).not.toContain("run_slash_command")
- })
-})
-
-describe("filterMcpToolsForMode", () => {
- const mockMcpTools: OpenAI.Chat.ChatCompletionTool[] = [
- {
- type: "function",
- function: {
- name: "mcp_server1_tool1",
- description: "MCP tool 1",
- parameters: {},
- },
- },
- {
- type: "function",
- function: {
- name: "mcp_server1_tool2",
- description: "MCP tool 2",
- parameters: {},
- },
- },
- ]
-
- it("should include MCP tools when mode has mcp group", () => {
- const modeWithMcp: ModeConfig = {
- slug: "test-with-mcp",
- name: "Test",
- roleDefinition: "Test",
- groups: ["read", "mcp"] as const,
- }
-
- const filtered = filterMcpToolsForMode(mockMcpTools, "test-with-mcp", [modeWithMcp], {})
-
- expect(filtered).toHaveLength(2)
- expect(filtered).toEqual(mockMcpTools)
- })
-
- it("should exclude MCP tools when mode does not have mcp group", () => {
- const modeWithoutMcp: ModeConfig = {
- slug: "test-no-mcp",
- name: "Test",
- roleDefinition: "Test",
- groups: ["read", "edit"] as const,
- }
-
- const filtered = filterMcpToolsForMode(mockMcpTools, "test-no-mcp", [modeWithoutMcp], {})
-
- expect(filtered).toHaveLength(0)
- })
-
- it("should handle undefined mode by using default mode", () => {
- // Default mode (code) has mcp group
- const filtered = filterMcpToolsForMode(mockMcpTools, undefined, undefined, {})
-
- // Should include MCP tools since default mode has mcp group
- expect(filtered.length).toBeGreaterThan(0)
- })
-
- describe("applyModelToolCustomization", () => {
- const codeMode: ModeConfig = {
- slug: "code",
- name: "Code",
- roleDefinition: "Test",
- groups: ["read", "edit", "browser", "command", "mcp"] as const,
- }
-
- const architectMode: ModeConfig = {
- slug: "architect",
- name: "Architect",
- roleDefinition: "Test",
- groups: ["read", "browser", "mcp"] as const,
- }
-
- it("should return original tools when modelInfo is undefined", () => {
- const tools = new Set(["read_file", "write_to_file", "apply_diff"])
- const result = applyModelToolCustomization(tools, codeMode, undefined)
- expect(result.allowedTools).toEqual(tools)
- })
-
- it("should exclude tools specified in excludedTools", () => {
- const tools = new Set(["read_file", "write_to_file", "apply_diff"])
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- excludedTools: ["apply_diff"],
- }
- const result = applyModelToolCustomization(tools, codeMode, modelInfo)
- expect(result.allowedTools.has("read_file")).toBe(true)
- expect(result.allowedTools.has("write_to_file")).toBe(true)
- expect(result.allowedTools.has("apply_diff")).toBe(false)
- })
-
- it("should exclude multiple tools", () => {
- const tools = new Set(["read_file", "write_to_file", "apply_diff", "execute_command"])
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- excludedTools: ["apply_diff", "write_to_file"],
- }
- const result = applyModelToolCustomization(tools, codeMode, modelInfo)
- expect(result.allowedTools.has("read_file")).toBe(true)
- expect(result.allowedTools.has("execute_command")).toBe(true)
- expect(result.allowedTools.has("write_to_file")).toBe(false)
- expect(result.allowedTools.has("apply_diff")).toBe(false)
- })
-
- it("should include tools only if they belong to allowed groups", () => {
- const tools = new Set(["read_file"])
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- includedTools: ["write_to_file", "apply_diff"], // Both in edit group
- }
- const result = applyModelToolCustomization(tools, codeMode, modelInfo)
- expect(result.allowedTools.has("read_file")).toBe(true)
- expect(result.allowedTools.has("write_to_file")).toBe(true)
- expect(result.allowedTools.has("apply_diff")).toBe(true)
- })
-
- it("should NOT include tools from groups not allowed by mode", () => {
- const tools = new Set(["read_file"])
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- includedTools: ["write_to_file", "apply_diff"], // Edit group tools
- }
- // Architect mode doesn't have edit group
- const result = applyModelToolCustomization(tools, architectMode, modelInfo)
- expect(result.allowedTools.has("read_file")).toBe(true)
- expect(result.allowedTools.has("write_to_file")).toBe(false) // Not in allowed groups
- expect(result.allowedTools.has("apply_diff")).toBe(false) // Not in allowed groups
- })
-
- it("should apply both exclude and include operations", () => {
- const tools = new Set(["read_file", "write_to_file", "apply_diff"])
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- excludedTools: ["apply_diff"],
- includedTools: ["search_and_replace"], // Another edit tool (customTool)
- }
- const result = applyModelToolCustomization(tools, codeMode, modelInfo)
- expect(result.allowedTools.has("read_file")).toBe(true)
- expect(result.allowedTools.has("write_to_file")).toBe(true)
- expect(result.allowedTools.has("apply_diff")).toBe(false) // Excluded
- expect(result.allowedTools.has("search_and_replace")).toBe(true) // Included
- })
-
- it("should handle empty excludedTools and includedTools arrays", () => {
- const tools = new Set(["read_file", "write_to_file"])
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- excludedTools: [],
- includedTools: [],
- }
- const result = applyModelToolCustomization(tools, codeMode, modelInfo)
- expect(result.allowedTools).toEqual(tools)
- })
-
- it("should ignore excluded tools that are not in the original set", () => {
- const tools = new Set(["read_file", "write_to_file"])
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- excludedTools: ["apply_diff", "nonexistent_tool"],
- }
- const result = applyModelToolCustomization(tools, codeMode, modelInfo)
- expect(result.allowedTools.has("read_file")).toBe(true)
- expect(result.allowedTools.has("write_to_file")).toBe(true)
- expect(result.allowedTools.size).toBe(2)
- })
-
- it("should NOT include customTools by default", () => {
- const tools = new Set(["read_file", "write_to_file"])
- // Assume 'edit' group has a customTool defined in TOOL_GROUPS
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- // No includedTools specified
- }
- const result = applyModelToolCustomization(tools, codeMode, modelInfo)
- // customTools should not be in the result unless explicitly included
- expect(result.allowedTools.has("read_file")).toBe(true)
- expect(result.allowedTools.has("write_to_file")).toBe(true)
- })
-
- it("should NOT include tools that are not in any TOOL_GROUPS", () => {
- const tools = new Set(["read_file"])
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- includedTools: ["my_custom_tool"], // Not in any tool group
- }
- const result = applyModelToolCustomization(tools, codeMode, modelInfo)
- expect(result.allowedTools.has("read_file")).toBe(true)
- expect(result.allowedTools.has("my_custom_tool")).toBe(false)
- })
-
- it("should NOT include undefined tools even with allowed groups", () => {
- const tools = new Set(["read_file"])
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- includedTools: ["custom_edit_tool"], // Not in any tool group
- }
- // Even though architect mode has read group, undefined tools are not added
- const result = applyModelToolCustomization(tools, architectMode, modelInfo)
- expect(result.allowedTools.has("read_file")).toBe(true)
- expect(result.allowedTools.has("custom_edit_tool")).toBe(false)
- })
-
- describe("with customTools defined in TOOL_GROUPS", () => {
- const originalToolGroups = { ...toolsModule.TOOL_GROUPS }
-
- beforeEach(() => {
- // Add a customTool to the edit group
- ;(toolsModule.TOOL_GROUPS as any).edit = {
- ...originalToolGroups.edit,
- customTools: ["special_edit_tool"],
- }
- })
-
- afterEach(() => {
- // Restore original TOOL_GROUPS
- ;(toolsModule.TOOL_GROUPS as any).edit = originalToolGroups.edit
- })
-
- it("should include customTools when explicitly specified in includedTools", () => {
- const tools = new Set(["read_file", "write_to_file"])
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- includedTools: ["special_edit_tool"], // customTool from edit group
- }
- const result = applyModelToolCustomization(tools, codeMode, modelInfo)
- expect(result.allowedTools.has("read_file")).toBe(true)
- expect(result.allowedTools.has("write_to_file")).toBe(true)
- expect(result.allowedTools.has("special_edit_tool")).toBe(true) // customTool should be included
- })
-
- it("should NOT include customTools when not specified in includedTools", () => {
- const tools = new Set(["read_file", "write_to_file"])
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- // No includedTools specified
- }
- const result = applyModelToolCustomization(tools, codeMode, modelInfo)
- expect(result.allowedTools.has("read_file")).toBe(true)
- expect(result.allowedTools.has("write_to_file")).toBe(true)
- expect(result.allowedTools.has("special_edit_tool")).toBe(false) // customTool should NOT be included by default
- })
-
- it("should NOT include customTools from groups not allowed by mode", () => {
- const tools = new Set(["read_file"])
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- includedTools: ["special_edit_tool"], // customTool from edit group
- }
- // Architect mode doesn't have edit group
- const result = applyModelToolCustomization(tools, architectMode, modelInfo)
- expect(result.allowedTools.has("read_file")).toBe(true)
- expect(result.allowedTools.has("special_edit_tool")).toBe(false) // customTool should NOT be included
- })
- })
- })
-
- describe("filterNativeToolsForMode with model customization", () => {
- const mockNativeTools: OpenAI.Chat.ChatCompletionTool[] = [
- {
- type: "function",
- function: {
- name: "read_file",
- description: "Read files",
- parameters: {},
- },
- },
- {
- type: "function",
- function: {
- name: "write_to_file",
- description: "Write files",
- parameters: {},
- },
- },
- {
- type: "function",
- function: {
- name: "apply_diff",
- description: "Apply diff",
- parameters: {},
- },
- },
- {
- type: "function",
- function: {
- name: "execute_command",
- description: "Execute command",
- parameters: {},
- },
- },
- {
- type: "function",
- function: {
- name: "search_and_replace",
- description: "Search and replace",
- parameters: {},
- },
- },
- {
- type: "function",
- function: {
- name: "edit_file",
- description: "Edit file",
- parameters: {},
- },
- },
- ]
-
- it("should exclude tools when model specifies excludedTools", () => {
- const codeMode: ModeConfig = {
- slug: "code",
- name: "Code",
- roleDefinition: "Test",
- groups: ["read", "edit", "browser", "command", "mcp"] as const,
- }
-
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- excludedTools: ["apply_diff"],
- }
-
- const filtered = filterNativeToolsForMode(mockNativeTools, "code", [codeMode], {}, undefined, {
- modelInfo,
- })
-
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
-
- expect(toolNames).toContain("read_file")
- expect(toolNames).toContain("write_to_file")
- expect(toolNames).not.toContain("apply_diff") // Excluded by model
- })
-
- it("should include tools when model specifies includedTools from allowed groups", () => {
- const modeWithOnlyRead: ModeConfig = {
- slug: "limited",
- name: "Limited",
- roleDefinition: "Test",
- groups: ["read", "edit"] as const,
- }
-
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- includedTools: ["search_and_replace"], // Edit group customTool
- }
-
- const filtered = filterNativeToolsForMode(mockNativeTools, "limited", [modeWithOnlyRead], {}, undefined, {
- modelInfo,
- })
-
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
-
- expect(toolNames).toContain("search_and_replace") // Included by model
- })
-
- it("should NOT include tools from groups not allowed by mode", () => {
- const architectMode: ModeConfig = {
- slug: "architect",
- name: "Architect",
- roleDefinition: "Test",
- groups: ["read", "browser"] as const, // No edit group
- }
-
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- includedTools: ["write_to_file", "apply_diff"], // Edit group tools
- }
-
- const filtered = filterNativeToolsForMode(mockNativeTools, "architect", [architectMode], {}, undefined, {
- modelInfo,
- })
-
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
-
- expect(toolNames).toContain("read_file")
- expect(toolNames).not.toContain("write_to_file") // Not in mode's allowed groups
- expect(toolNames).not.toContain("apply_diff") // Not in mode's allowed groups
- })
-
- it("should combine excludedTools and includedTools", () => {
- const codeMode: ModeConfig = {
- slug: "code",
- name: "Code",
- roleDefinition: "Test",
- groups: ["read", "edit", "browser", "command", "mcp"] as const,
- }
-
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- excludedTools: ["apply_diff"],
- includedTools: ["search_and_replace"],
- }
-
- const filtered = filterNativeToolsForMode(mockNativeTools, "code", [codeMode], {}, undefined, {
- modelInfo,
- })
-
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
-
- expect(toolNames).toContain("write_to_file")
- expect(toolNames).toContain("search_and_replace") // Included
- expect(toolNames).not.toContain("apply_diff") // Excluded
- })
-
- it("should honor included aliases while respecting exclusions", () => {
- const codeMode: ModeConfig = {
- slug: "code",
- name: "Code",
- roleDefinition: "Test",
- groups: ["read", "edit", "browser", "command", "mcp"] as const,
- }
-
- const modelInfo: ModelInfo = {
- contextWindow: 100000,
- supportsPromptCache: false,
- excludedTools: ["apply_diff"],
- includedTools: ["edit_file", "write_file"],
- }
-
- const filtered = filterNativeToolsForMode(mockNativeTools, "code", [codeMode], {}, undefined, {
- modelInfo,
- })
-
- const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
-
- expect(toolNames).toContain("edit_file")
- expect(toolNames).toContain("write_file")
- expect(toolNames).not.toContain("apply_diff")
- expect(toolNames).not.toContain("write_to_file")
- })
- })
-})
-
-describe("resolveToolAlias", () => {
- it("should resolve known alias to canonical name", () => {
- // write_file is an alias for write_to_file (defined in TOOL_ALIASES)
- expect(resolveToolAlias("write_file")).toBe("write_to_file")
- })
-
- it("should return canonical name unchanged", () => {
- expect(resolveToolAlias("write_to_file")).toBe("write_to_file")
- expect(resolveToolAlias("read_file")).toBe("read_file")
- expect(resolveToolAlias("apply_diff")).toBe("apply_diff")
- })
-
- it("should return unknown tool names unchanged", () => {
- expect(resolveToolAlias("unknown_tool")).toBe("unknown_tool")
- expect(resolveToolAlias("custom_tool_xyz")).toBe("custom_tool_xyz")
- })
-
- it("should ensure allowedFunctionNames are consistent with functionDeclarations", () => {
- // This test documents the fix for the Gemini allowedFunctionNames issue.
- // When tools are renamed via aliasRenames, the alias names must be resolved
- // back to canonical names for allowedFunctionNames to match functionDeclarations.
- //
- // Example scenario:
- // - Model specifies includedTools: ["write_file"] (an alias)
- // - filterNativeToolsForMode returns tool with name "write_file"
- // - But allTools (functionDeclarations) contains "write_to_file" (canonical)
- // - If allowedFunctionNames contains "write_file", Gemini will error
- // - Resolving aliases ensures consistency: resolveToolAlias("write_file") -> "write_to_file"
-
- const aliasToolName = "write_file"
- const canonicalToolName = "write_to_file"
-
- // Simulate extracting name from a filtered tool that was renamed to alias
- const extractedName = aliasToolName
-
- // Before the fix: allowedFunctionNames would contain alias name
- // This would cause Gemini to error because "write_file" doesn't exist in functionDeclarations
-
- // After the fix: we resolve to canonical name
- const resolvedName = resolveToolAlias(extractedName)
-
- // The resolved name matches what's in functionDeclarations (canonical names)
- expect(resolvedName).toBe(canonicalToolName)
- })
-})
diff --git a/src/core/prompts/tools/__tests__/new-task.spec.ts b/src/core/prompts/tools/__tests__/new-task.spec.ts
deleted file mode 100644
index c110cffcd1..0000000000
--- a/src/core/prompts/tools/__tests__/new-task.spec.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { getNewTaskDescription } from "../new-task"
-import { ToolArgs } from "../types"
-
-describe("getNewTaskDescription", () => {
- it("should NOT show todos parameter at all when setting is disabled", () => {
- const args: ToolArgs = {
- cwd: "/test",
- supportsComputerUse: false,
- settings: {
- newTaskRequireTodos: false,
- },
- }
-
- const description = getNewTaskDescription(args)
-
- // Check that todos parameter is NOT shown at all
- expect(description).not.toContain("todos:")
- expect(description).not.toContain("todos parameter")
- expect(description).not.toContain("The initial todo list in markdown checklist format")
-
- // Should have a simple example without todos
- expect(description).toContain("Implement a new feature for the application")
-
- // Should NOT have any todos tags in examples
- expect(description).not.toContain("")
- expect(description).not.toContain("")
-
- // Should still have mode and message as required
- expect(description).toContain("mode: (required)")
- expect(description).toContain("message: (required)")
- })
-
- it("should show todos as required when setting is enabled", () => {
- const args: ToolArgs = {
- cwd: "/test",
- supportsComputerUse: false,
- settings: {
- newTaskRequireTodos: true,
- },
- }
-
- const description = getNewTaskDescription(args)
-
- // Check that todos is marked as required
- expect(description).toContain("todos: (required)")
- expect(description).toContain("and initial todo list")
- expect(description).toContain("The initial todo list in markdown checklist format")
-
- // Should not contain any mention of optional for todos
- expect(description).not.toContain("todos: (optional)")
- expect(description).not.toContain("optional initial todo list")
-
- // Should include todos in the example
- expect(description).toContain("")
- expect(description).toContain("")
- expect(description).toContain("Set up auth middleware")
- })
-
- it("should NOT show todos parameter when settings is undefined", () => {
- const args: ToolArgs = {
- cwd: "/test",
- supportsComputerUse: false,
- settings: undefined,
- }
-
- const description = getNewTaskDescription(args)
-
- // Check that todos parameter is NOT shown by default
- expect(description).not.toContain("todos:")
- expect(description).not.toContain("The initial todo list in markdown checklist format")
- expect(description).not.toContain("")
- expect(description).not.toContain("")
- })
-
- it("should NOT show todos parameter when newTaskRequireTodos is undefined", () => {
- const args: ToolArgs = {
- cwd: "/test",
- supportsComputerUse: false,
- settings: {},
- }
-
- const description = getNewTaskDescription(args)
-
- // Check that todos parameter is NOT shown by default
- expect(description).not.toContain("todos:")
- expect(description).not.toContain("The initial todo list in markdown checklist format")
- expect(description).not.toContain("")
- expect(description).not.toContain("")
- })
-
- it("should include todos in examples only when setting is enabled", () => {
- const argsWithSettingOff: ToolArgs = {
- cwd: "/test",
- supportsComputerUse: false,
- settings: {
- newTaskRequireTodos: false,
- },
- }
-
- const argsWithSettingOn: ToolArgs = {
- cwd: "/test",
- supportsComputerUse: false,
- settings: {
- newTaskRequireTodos: true,
- },
- }
-
- const descriptionOff = getNewTaskDescription(argsWithSettingOff)
- const descriptionOn = getNewTaskDescription(argsWithSettingOn)
-
- // When setting is on, should include todos in main example
- expect(descriptionOn).toContain("Implement user authentication")
- expect(descriptionOn).toContain("[ ] Set up auth middleware")
- expect(descriptionOn).toContain("")
- expect(descriptionOn).toContain("")
-
- // When setting is off, should NOT include any todos references
- expect(descriptionOff).not.toContain("")
- expect(descriptionOff).not.toContain("")
- expect(descriptionOff).not.toContain("[ ] Set up auth middleware")
- expect(descriptionOff).not.toContain("[ ] First task to complete")
-
- // When setting is off, main example should be simple
- const usagePattern = /\s*.*<\/mode>\s*.*<\/message>\s*<\/new_task>/s
- expect(descriptionOff).toMatch(usagePattern)
- })
-})
diff --git a/src/core/prompts/tools/access-mcp-resource.ts b/src/core/prompts/tools/access-mcp-resource.ts
deleted file mode 100644
index 3807aab6bd..0000000000
--- a/src/core/prompts/tools/access-mcp-resource.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { ToolArgs } from "./types"
-import { McpHub } from "../../../services/mcp/McpHub"
-
-/**
- * Helper function to check if any MCP server has resources available
- */
-function hasAnyMcpResources(mcpHub: McpHub): boolean {
- const servers = mcpHub.getServers()
- return servers.some((server) => server.resources && server.resources.length > 0)
-}
-
-export function getAccessMcpResourceDescription(args: ToolArgs): string | undefined {
- if (!args.mcpHub || !hasAnyMcpResources(args.mcpHub)) {
- return undefined
- }
- return `## 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
-`
-}
diff --git a/src/core/prompts/tools/ask-followup-question.ts b/src/core/prompts/tools/ask-followup-question.ts
deleted file mode 100644
index c40684b8bc..0000000000
--- a/src/core/prompts/tools/ask-followup-question.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-export function getAskFollowupQuestionDescription(): string {
- return `## 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
-
-`
-}
diff --git a/src/core/prompts/tools/attempt-completion.ts b/src/core/prompts/tools/attempt-completion.ts
deleted file mode 100644
index 62f0827f98..0000000000
--- a/src/core/prompts/tools/attempt-completion.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { ToolArgs } from "./types"
-
-export function getAttemptCompletionDescription(args?: ToolArgs): string {
- return `## 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
-
-`
-}
diff --git a/src/core/prompts/tools/browser-action.ts b/src/core/prompts/tools/browser-action.ts
deleted file mode 100644
index 88c7343d0a..0000000000
--- a/src/core/prompts/tools/browser-action.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-import { ToolArgs } from "./types"
-
-export function getBrowserActionDescription(args: ToolArgs): string | undefined {
- if (!args.supportsComputerUse) {
- return undefined
- }
- return `## 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
-`
-}
diff --git a/src/core/prompts/tools/codebase-search.ts b/src/core/prompts/tools/codebase-search.ts
deleted file mode 100644
index f613039215..0000000000
--- a/src/core/prompts/tools/codebase-search.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { ToolArgs } from "./types"
-
-export function getCodebaseSearchDescription(args: ToolArgs): string {
- return `## codebase_search
-Description: Find files most relevant to the search query using semantic search. Searches based on meaning rather than exact text matches. By default searches entire workspace. Reuse the user's exact wording unless there's a clear reason not to - their phrasing often helps semantic search. Queries MUST be in English (translate if needed).
-
-**CRITICAL: For ANY exploration of code you haven't examined yet in this conversation, you MUST use this tool FIRST before any other search or file exploration tools.** This applies throughout the entire conversation, not just at the beginning. This tool uses semantic search to find relevant code based on meaning rather than just keywords, making it far more effective than regex-based search_files for understanding implementations. Even if you've already explored some code, any new area of exploration requires codebase_search first.
-
-Parameters:
-- query: (required) The search query. Reuse the user's exact wording/question format unless there's a clear reason not to.
-- path: (optional) Limit search to specific subdirectory (relative to the current workspace directory ${args.cwd}). Leave empty for entire workspace.
-
-Usage:
-
-Your natural language query here
-Optional subdirectory path
-
-
-Example: Searching for user authentication code
-
-User login and password hashing
-src/auth
-
-
-Example: Searching entire workspace
-
-database connection pooling
-
-
-`
-}
diff --git a/src/core/prompts/tools/execute-command.ts b/src/core/prompts/tools/execute-command.ts
deleted file mode 100644
index c1fc1ea3f1..0000000000
--- a/src/core/prompts/tools/execute-command.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { ToolArgs } from "./types"
-
-export function getExecuteCommandDescription(args: ToolArgs): string | undefined {
- return `## execute_command
-Description: Request to execute a CLI command on the system. Use this when you need to perform system operations or run specific commands to accomplish any step in the user's task. You must tailor your command to the user's system and provide a clear explanation of what the command does. For command chaining, use the appropriate chaining syntax for the user's shell. Prefer to execute complex CLI commands over creating executable scripts, as they are more flexible and easier to run. Prefer relative commands and paths that avoid location sensitivity for terminal consistency, e.g: \`touch ./testdata/example.file\`, \`dir ./examples/model1/data/yaml\`, or \`go test ./cmd/front --config ./cmd/front/config.yml\`. If directed by the user, you may open a terminal in a different directory by using the \`cwd\` parameter.
-Parameters:
-- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
-- cwd: (optional) The working directory to execute the command in (default: ${args.cwd})
-Usage:
-
-Your command here
-Working directory path (optional)
-
-
-Example: Requesting to execute npm run dev
-
-npm run dev
-
-
-Example: Requesting to execute ls in a specific directory if directed
-
-ls -la
-/home/user/projects
-`
-}
diff --git a/src/core/prompts/tools/fetch-instructions.ts b/src/core/prompts/tools/fetch-instructions.ts
deleted file mode 100644
index dd9cbb80da..0000000000
--- a/src/core/prompts/tools/fetch-instructions.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * Generates the fetch_instructions tool description.
- * @param enableMcpServerCreation - Whether to include MCP server creation task.
- * Defaults to true when undefined.
- */
-export function getFetchInstructionsDescription(enableMcpServerCreation?: boolean): string {
- const tasks =
- enableMcpServerCreation !== false
- ? ` create_mcp_server
- create_mode`
- : ` create_mode`
-
- const example =
- enableMcpServerCreation !== false
- ? `Example: Requesting instructions to create an MCP Server
-
-
-create_mcp_server
-`
- : `Example: Requesting instructions to create a Mode
-
-
-create_mode
-`
-
- return `## 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:
-${tasks}
-
-${example}`
-}
diff --git a/src/core/prompts/tools/filter-tools-for-mode.ts b/src/core/prompts/tools/filter-tools-for-mode.ts
index f296c1b5c5..5560fe9bc6 100644
--- a/src/core/prompts/tools/filter-tools-for-mode.ts
+++ b/src/core/prompts/tools/filter-tools-for-mode.ts
@@ -211,7 +211,7 @@ export function applyModelToolCustomization(
/**
* Filters native tools based on mode restrictions and model customization.
- * This ensures native tools are filtered the same way XML tools are filtered in the system prompt.
+ * This ensures native tools are filtered consistently with mode/tool permissions.
*
* @param nativeTools - Array of all available native tools
* @param mode - Current mode slug
@@ -296,11 +296,6 @@ export function filterNativeToolsForMode(
allowedToolNames.delete("browser_action")
}
- // Conditionally exclude apply_diff if diffs are disabled
- if (settings?.diffEnabled === false) {
- allowedToolNames.delete("apply_diff")
- }
-
// Conditionally exclude access_mcp_resource if MCP is not enabled or there are no resources
if (!mcpHub || !hasAnyMcpResources(mcpHub)) {
allowedToolNames.delete("access_mcp_resource")
diff --git a/src/core/prompts/tools/generate-image.ts b/src/core/prompts/tools/generate-image.ts
deleted file mode 100644
index 458b7ae8cf..0000000000
--- a/src/core/prompts/tools/generate-image.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import { ToolArgs } from "./types"
-
-export function getGenerateImageDescription(args: ToolArgs): string {
- return `## generate_image
-Description: Request to generate or edit an image using AI models through OpenRouter API. This tool can create new images from text prompts or modify existing images based on your instructions. When an input image is provided, the AI will apply the requested edits, transformations, or enhancements to that image.
-Parameters:
-- prompt: (required) The text prompt describing what to generate or how to edit the image
-- path: (required) The file path where the generated/edited image should be saved (relative to the current workspace directory ${args.cwd}). The tool will automatically add the appropriate image extension if not provided.
-- image: (optional) The file path to an input image to edit or transform (relative to the current workspace directory ${args.cwd}). Supported formats: PNG, JPG, JPEG, GIF, WEBP.
-Usage:
-
-Your image description here
-path/to/save/image.png
-path/to/input/image.jpg
-
-
-Example: Requesting to generate a sunset image
-
-A beautiful sunset over mountains with vibrant orange and purple colors
-images/sunset.png
-
-
-Example: Editing an existing image
-
-Transform this image into a watercolor painting style
-images/watercolor-output.png
-images/original-photo.jpg
-
-
-Example: Upscaling and enhancing an image
-
-Upscale this image to higher resolution, enhance details, improve clarity and sharpness while maintaining the original content and composition
-images/enhanced-photo.png
-images/low-res-photo.jpg
-`
-}
diff --git a/src/core/prompts/tools/index.ts b/src/core/prompts/tools/index.ts
deleted file mode 100644
index b75725a99b..0000000000
--- a/src/core/prompts/tools/index.ts
+++ /dev/null
@@ -1,172 +0,0 @@
-import type { ToolName, ModeConfig } from "@roo-code/types"
-
-import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, DiffStrategy } from "../../../shared/tools"
-import { Mode, getModeConfig, getGroupName } from "../../../shared/modes"
-
-import { isToolAllowedForMode } from "../../tools/validateToolUse"
-
-import { McpHub } from "../../../services/mcp/McpHub"
-import { CodeIndexManager } from "../../../services/code-index/manager"
-
-import { ToolArgs } from "./types"
-import { getExecuteCommandDescription } from "./execute-command"
-import { getReadFileDescription } from "./read-file"
-import { getFetchInstructionsDescription } from "./fetch-instructions"
-import { getWriteToFileDescription } from "./write-to-file"
-import { getSearchFilesDescription } from "./search-files"
-import { getListFilesDescription } from "./list-files"
-import { getBrowserActionDescription } from "./browser-action"
-import { getAskFollowupQuestionDescription } from "./ask-followup-question"
-import { getAttemptCompletionDescription } from "./attempt-completion"
-import { getUseMcpToolDescription } from "./use-mcp-tool"
-import { getAccessMcpResourceDescription } from "./access-mcp-resource"
-import { getSwitchModeDescription } from "./switch-mode"
-import { getNewTaskDescription } from "./new-task"
-import { getCodebaseSearchDescription } from "./codebase-search"
-import { getUpdateTodoListDescription } from "./update-todo-list"
-import { getRunSlashCommandDescription } from "./run-slash-command"
-import { getGenerateImageDescription } from "./generate-image"
-
-// Map of tool names to their description functions
-const toolDescriptionMap: Record string | undefined> = {
- execute_command: (args) => getExecuteCommandDescription(args),
- read_file: (args) => getReadFileDescription(args),
- fetch_instructions: (args) => getFetchInstructionsDescription(args.settings?.enableMcpServerCreation),
- write_to_file: (args) => getWriteToFileDescription(args),
- search_files: (args) => getSearchFilesDescription(args),
- list_files: (args) => getListFilesDescription(args),
- browser_action: (args) => getBrowserActionDescription(args),
- ask_followup_question: () => getAskFollowupQuestionDescription(),
- attempt_completion: (args) => getAttemptCompletionDescription(args),
- use_mcp_tool: (args) => getUseMcpToolDescription(args),
- access_mcp_resource: (args) => getAccessMcpResourceDescription(args),
- codebase_search: (args) => getCodebaseSearchDescription(args),
- switch_mode: () => getSwitchModeDescription(),
- new_task: (args) => getNewTaskDescription(args),
- apply_diff: (args) =>
- args.diffStrategy ? args.diffStrategy.getToolDescription({ cwd: args.cwd, toolOptions: args.toolOptions }) : "",
- update_todo_list: (args) => getUpdateTodoListDescription(args),
- run_slash_command: () => getRunSlashCommandDescription(),
- generate_image: (args) => getGenerateImageDescription(args),
-}
-
-export function getToolDescriptionsForMode(
- mode: Mode,
- cwd: string,
- supportsComputerUse: boolean,
- codeIndexManager?: CodeIndexManager,
- diffStrategy?: DiffStrategy,
- browserViewportSize?: string,
- mcpHub?: McpHub,
- customModes?: ModeConfig[],
- experiments?: Record,
- partialReadsEnabled?: boolean,
- settings?: Record,
- enableMcpServerCreation?: boolean,
- modelId?: string,
-): string {
- const config = getModeConfig(mode, customModes)
- const args: ToolArgs = {
- cwd,
- supportsComputerUse,
- diffStrategy,
- browserViewportSize,
- mcpHub,
- partialReadsEnabled,
- settings: {
- ...settings,
- enableMcpServerCreation,
- modelId,
- },
- experiments,
- }
-
- const tools = new Set()
-
- // Add tools from mode's groups
- config.groups.forEach((groupEntry) => {
- const groupName = getGroupName(groupEntry)
- const toolGroup = TOOL_GROUPS[groupName]
- if (toolGroup) {
- toolGroup.tools.forEach((tool) => {
- if (
- isToolAllowedForMode(
- tool as ToolName,
- mode,
- customModes ?? [],
- undefined,
- undefined,
- experiments ?? {},
- )
- ) {
- tools.add(tool)
- }
- })
- }
- })
-
- // Add always available tools
- ALWAYS_AVAILABLE_TOOLS.forEach((tool) => tools.add(tool))
-
- // Conditionally exclude codebase_search if feature is disabled or not configured
- if (
- !codeIndexManager ||
- !(codeIndexManager.isFeatureEnabled && codeIndexManager.isFeatureConfigured && codeIndexManager.isInitialized)
- ) {
- tools.delete("codebase_search")
- }
-
- // Conditionally exclude update_todo_list if disabled in settings
- if (settings?.todoListEnabled === false) {
- tools.delete("update_todo_list")
- }
-
- // Conditionally exclude generate_image if experiment is not enabled
- if (!experiments?.imageGeneration) {
- tools.delete("generate_image")
- }
-
- // Conditionally exclude run_slash_command if experiment is not enabled
- if (!experiments?.runSlashCommand) {
- tools.delete("run_slash_command")
- }
-
- // Map tool descriptions for allowed tools
- const descriptions = Array.from(tools).map((toolName) => {
- const descriptionFn = toolDescriptionMap[toolName]
- if (!descriptionFn) {
- return undefined
- }
-
- const description = descriptionFn({
- ...args,
- toolOptions: undefined, // No tool options in group-based approach
- })
-
- return description
- })
-
- return `# Tools\n\n${descriptions.filter(Boolean).join("\n\n")}`
-}
-
-// Export individual description functions for backward compatibility
-export {
- getExecuteCommandDescription,
- getReadFileDescription,
- getFetchInstructionsDescription,
- getWriteToFileDescription,
- getSearchFilesDescription,
- getListFilesDescription,
- getBrowserActionDescription,
- getAskFollowupQuestionDescription,
- getAttemptCompletionDescription,
- getUseMcpToolDescription,
- getAccessMcpResourceDescription,
- getSwitchModeDescription,
- getCodebaseSearchDescription,
- getRunSlashCommandDescription,
- getGenerateImageDescription,
-}
-
-// Export native tool definitions (JSON schema format for OpenAI-compatible APIs)
-export { nativeTools } from "./native-tools"
diff --git a/src/core/prompts/tools/list-files.ts b/src/core/prompts/tools/list-files.ts
deleted file mode 100644
index 96c43ea4a6..0000000000
--- a/src/core/prompts/tools/list-files.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { ToolArgs } from "./types"
-
-export function getListFilesDescription(args: ToolArgs): string {
- return `## 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 ${args.cwd})
-- 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
-`
-}
diff --git a/src/core/prompts/tools/native-tools/__tests__/mcp_server.spec.ts b/src/core/prompts/tools/native-tools/__tests__/mcp_server.spec.ts
index 932468cd9b..ddd7caaccf 100644
--- a/src/core/prompts/tools/native-tools/__tests__/mcp_server.spec.ts
+++ b/src/core/prompts/tools/native-tools/__tests__/mcp_server.spec.ts
@@ -89,7 +89,7 @@ describe("getMcpServerTools", () => {
// Should only have one tool (from project server)
expect(result).toHaveLength(1)
- expect(getFunction(result[0]).name).toBe("mcp--context7--resolve___library___id")
+ expect(getFunction(result[0]).name).toBe("mcp--context7--resolve-library-id")
// Project server takes priority
expect(getFunction(result[0]).description).toBe("Project description")
})
diff --git a/src/core/prompts/tools/new-task.ts b/src/core/prompts/tools/new-task.ts
deleted file mode 100644
index bba6c6250f..0000000000
--- a/src/core/prompts/tools/new-task.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { ToolArgs } from "./types"
-
-/**
- * Prompt when todos are NOT required (default)
- */
-const PROMPT_WITHOUT_TODOS = `## 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
-
-`
-
-/**
- * Prompt when todos ARE required
- */
-const PROMPT_WITH_TODOS = `## new_task
-Description: This will let you create a new task instance in the chosen mode using your provided message and initial todo list.
-
-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.
-- todos: (required) The initial todo list in markdown checklist format for the new task.
-
-Usage:
-
-your-mode-slug-here
-Your initial instructions here
-
-[ ] First task to complete
-[ ] Second task to complete
-[ ] Third task to complete
-
-
-
-Example:
-
-code
-Implement user authentication
-
-[ ] Set up auth middleware
-[ ] Create login endpoint
-[ ] Add session management
-[ ] Write tests
-
-
-
-`
-
-export function getNewTaskDescription(args: ToolArgs): string {
- const todosRequired = args.settings?.newTaskRequireTodos === true
-
- // Simply return the appropriate prompt based on the setting
- return todosRequired ? PROMPT_WITH_TODOS : PROMPT_WITHOUT_TODOS
-}
diff --git a/src/core/prompts/tools/read-file.ts b/src/core/prompts/tools/read-file.ts
deleted file mode 100644
index 86f4dc8c64..0000000000
--- a/src/core/prompts/tools/read-file.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { ToolArgs } from "./types"
-
-export function getReadFileDescription(args: ToolArgs): string {
- const maxConcurrentReads = args.settings?.maxConcurrentFileReads ?? 5
- const isMultipleReadsEnabled = maxConcurrentReads > 1
-
- return `## read_file
-Description: Request to read the contents of ${isMultipleReadsEnabled ? "one or more files" : "a file"}. The tool outputs line-numbered content (e.g. "1 | const x = 1") for easy reference when creating diffs or discussing code.${args.partialReadsEnabled ? " 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.
-
-${isMultipleReadsEnabled ? `**IMPORTANT: You can read a maximum of ${maxConcurrentReads} files in a single request.** If you need to read more files, use multiple sequential read_file requests.` : "**IMPORTANT: Multiple file reads are currently disabled. You can only read one file at a time.**"}
-
-${args.partialReadsEnabled ? `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 ${args.cwd})
- ${args.partialReadsEnabled ? `- line_range: (optional) One or more line range elements in format "start-end" (1-based, inclusive)` : ""}
-
-Usage:
-
-
-
- path/to/file
- ${args.partialReadsEnabled ? `start-end` : ""}
-
-
-
-
-Examples:
-
-1. Reading a single file:
-
-
-
- src/app.ts
- ${args.partialReadsEnabled ? `1-1000` : ""}
-
-
-
-
-${isMultipleReadsEnabled ? `2. Reading multiple files (within the ${maxConcurrentReads}-file limit):` : ""}${
- isMultipleReadsEnabled
- ? `
-
-
-
- src/app.ts
- ${
- args.partialReadsEnabled
- ? `1-50
- 100-150`
- : ""
- }
-
-
- src/utils.ts
- ${args.partialReadsEnabled ? `10-20` : ""}
-
-
-`
- : ""
- }
-
-${isMultipleReadsEnabled ? "3. " : "2. "}Reading an entire file:
-
-
-
- config.json
-
-
-
-
-IMPORTANT: You MUST use this Efficient Reading Strategy:
-- ${isMultipleReadsEnabled ? `You MUST read all related files and implementations together in a single operation (up to ${maxConcurrentReads} files at once)` : "You MUST read files one at a time, as multiple file reads are currently disabled"}
-- You MUST obtain all necessary context before proceeding with changes
-${
- args.partialReadsEnabled
- ? `- 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
-`
- : ""
-}
-${isMultipleReadsEnabled ? `- When you need to read more than ${maxConcurrentReads} files, prioritize the most critical files first, then use subsequent read_file requests for additional files` : ""}`
-}
diff --git a/src/core/prompts/tools/run-slash-command.ts b/src/core/prompts/tools/run-slash-command.ts
deleted file mode 100644
index 27047dcbaa..0000000000
--- a/src/core/prompts/tools/run-slash-command.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * Generates the run_slash_command tool description.
- */
-export function getRunSlashCommandDescription(): string {
- return `## run_slash_command
-Description: Execute a slash command to get specific instructions or content. Slash commands are predefined templates that provide detailed guidance for common tasks.
-
-Parameters:
-- command: (required) The name of the slash command to execute (e.g., "init", "test", "deploy")
-- args: (optional) Additional arguments or context to pass to the command
-
-Usage:
-
-command_name
-optional arguments
-
-
-Examples:
-
-1. Running the init command to analyze a codebase:
-
-init
-
-
-2. Running a command with additional context:
-
-test
-focus on integration tests
-
-
-The command content will be returned for you to execute or follow as instructions.`
-}
diff --git a/src/core/prompts/tools/search-files.ts b/src/core/prompts/tools/search-files.ts
deleted file mode 100644
index f0af9f8a23..0000000000
--- a/src/core/prompts/tools/search-files.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { ToolArgs } from "./types"
-
-export function getSearchFilesDescription(args: ToolArgs): string {
- return `## 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 ${args.cwd}). 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
-`
-}
diff --git a/src/core/prompts/tools/switch-mode.ts b/src/core/prompts/tools/switch-mode.ts
deleted file mode 100644
index a8c64d1e10..0000000000
--- a/src/core/prompts/tools/switch-mode.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-export function getSwitchModeDescription(): string {
- return `## 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
-`
-}
diff --git a/src/core/prompts/tools/types.ts b/src/core/prompts/tools/types.ts
deleted file mode 100644
index 9471d100d7..0000000000
--- a/src/core/prompts/tools/types.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { DiffStrategy } from "../../../shared/tools"
-import { McpHub } from "../../../services/mcp/McpHub"
-
-export type ToolArgs = {
- cwd: string
- supportsComputerUse: boolean
- diffStrategy?: DiffStrategy
- browserViewportSize?: string
- mcpHub?: McpHub
- toolOptions?: any
- partialReadsEnabled?: boolean
- settings?: Record
- experiments?: Record
-}
diff --git a/src/core/prompts/tools/update-todo-list.ts b/src/core/prompts/tools/update-todo-list.ts
deleted file mode 100644
index 30100617df..0000000000
--- a/src/core/prompts/tools/update-todo-list.ts
+++ /dev/null
@@ -1,76 +0,0 @@
-import { ToolArgs } from "./types"
-
-/**
- * Get the description for the update_todo_list tool.
- */
-export function getUpdateTodoListDescription(args?: ToolArgs): string {
- return `## 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.
-`
-}
diff --git a/src/core/prompts/tools/use-mcp-tool.ts b/src/core/prompts/tools/use-mcp-tool.ts
deleted file mode 100644
index ac9ef5b075..0000000000
--- a/src/core/prompts/tools/use-mcp-tool.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { ToolArgs } from "./types"
-
-export function getUseMcpToolDescription(args: ToolArgs): string | undefined {
- if (!args.mcpHub) {
- return undefined
- }
- return `## 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
-}
-
-`
-}
diff --git a/src/core/prompts/tools/write-to-file.ts b/src/core/prompts/tools/write-to-file.ts
deleted file mode 100644
index 49ca1169f1..0000000000
--- a/src/core/prompts/tools/write-to-file.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import { ToolArgs } from "./types"
-
-export function getWriteToFileDescription(args: ToolArgs): string {
- return `## 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 ${args.cwd})
-- 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"
-}
-
-`
-}
diff --git a/src/core/prompts/types.ts b/src/core/prompts/types.ts
index 0e27910c01..d438735f27 100644
--- a/src/core/prompts/types.ts
+++ b/src/core/prompts/types.ts
@@ -1,5 +1,3 @@
-import { ToolProtocol } from "@roo-code/types"
-
/**
* Settings passed to system prompt generation functions
*/
@@ -11,7 +9,6 @@ export interface SystemPromptSettings {
/** When true, recursively discover and load .roo/rules from subdirectories */
enableSubfolderRules?: boolean
newTaskRequireTodos: boolean
- toolProtocol?: ToolProtocol
/** When true, model should hide vendor/company identity in responses */
isStealthModel?: boolean
}
diff --git a/src/core/task-persistence/taskMetadata.ts b/src/core/task-persistence/taskMetadata.ts
index cf8d9adb52..4b77126971 100644
--- a/src/core/task-persistence/taskMetadata.ts
+++ b/src/core/task-persistence/taskMetadata.ts
@@ -1,7 +1,7 @@
import NodeCache from "node-cache"
import getFolderSize from "get-folder-size"
-import type { ClineMessage, HistoryItem, ToolProtocol } from "@roo-code/types"
+import type { ClineMessage, HistoryItem } from "@roo-code/types"
import { combineApiRequests } from "../../shared/combineApiRequests"
import { combineCommandSequences } from "../../shared/combineCommandSequences"
@@ -25,11 +25,6 @@ export type TaskMetadataOptions = {
apiConfigName?: string
/** Initial status for the task (e.g., "active" for child tasks) */
initialStatus?: "active" | "delegated" | "completed"
- /**
- * The tool protocol locked to this task. Once set, the task will
- * continue using this protocol even if user settings change.
- */
- toolProtocol?: ToolProtocol
}
export async function taskMetadata({
@@ -43,7 +38,6 @@ export async function taskMetadata({
mode,
apiConfigName,
initialStatus,
- toolProtocol,
}: TaskMetadataOptions) {
const taskDir = await getTaskDirectoryPath(globalStoragePath, id)
@@ -99,8 +93,6 @@ export async function taskMetadata({
// initialStatus is included when provided (e.g., "active" for child tasks)
// to ensure the status is set from the very first save, avoiding race conditions
// where attempt_completion might run before a separate status update.
- // toolProtocol is persisted to ensure tasks resume with the correct protocol
- // even if user settings have changed.
const historyItem: HistoryItem = {
id,
rootTaskId,
@@ -118,7 +110,6 @@ export async function taskMetadata({
size: taskDirSize,
workspace,
mode,
- ...(toolProtocol && { toolProtocol }),
...(typeof apiConfigName === "string" && apiConfigName.length > 0 ? { apiConfigName } : {}),
...(initialStatus && { status: initialStatus }),
}
diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts
index af7aed86d5..d2be23714e 100644
--- a/src/core/task/Task.ts
+++ b/src/core/task/Task.ts
@@ -33,7 +33,6 @@ import {
type HistoryItem,
type CreateTaskOptions,
type ModelInfo,
- type ToolProtocol,
type ClineApiReqCancelReason,
type ClineApiReqInfo,
RooCodeEventName,
@@ -45,18 +44,17 @@ import {
isIdleAsk,
isInteractiveAsk,
isResumableAsk,
- isNativeProtocol,
QueuedMessage,
DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
MAX_CHECKPOINT_TIMEOUT_SECONDS,
MIN_CHECKPOINT_TIMEOUT_SECONDS,
- TOOL_PROTOCOL,
ConsecutiveMistakeError,
+ MAX_MCP_TOOLS_THRESHOLD,
+ countEnabledMcpTools,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"
import { CloudService, BridgeOrchestrator } from "@roo-code/cloud"
-import { resolveToolProtocol, detectToolProtocolFromHistory } from "../../utils/resolveToolProtocol"
// api
import { ApiHandler, ApiHandlerCreateMessageMetadata, buildApiHandler } from "../../api"
@@ -72,7 +70,6 @@ import { getApiMetrics, hasTokenUsageChanged, hasToolUsageChanged } from "../../
import { ClineAskResponse } from "../../shared/WebviewMessage"
import { defaultModeSlug, getModeBySlug, getGroupName } from "../../shared/modes"
import { DiffStrategy, type ToolUse, type ToolParamName, toolParamNames } from "../../shared/tools"
-import { EXPERIMENT_IDS, experiments } from "../../shared/experiments"
import { getModelMaxOutputTokens } from "../../shared/api"
// services
@@ -105,12 +102,10 @@ import { FileContextTracker } from "../context-tracking/FileContextTracker"
import { RooIgnoreController } from "../ignore/RooIgnoreController"
import { RooProtectedController } from "../protect/RooProtectedController"
import { type AssistantMessageContent, presentAssistantMessage } from "../assistant-message"
-import { AssistantMessageParser } from "../assistant-message/AssistantMessageParser"
import { NativeToolCallParser } from "../assistant-message/NativeToolCallParser"
import { manageContext, willManageContext } from "../context-management"
import { ClineProvider } from "../webview/ClineProvider"
import { MultiSearchReplaceDiffStrategy } from "../diff/strategies/multi-search-replace"
-import { MultiFileSearchReplaceDiffStrategy } from "../diff/strategies/multi-file-search-replace"
import {
type ApiMessage,
readApiMessages,
@@ -135,6 +130,7 @@ import { MessageQueueService } from "../message-queue/MessageQueueService"
import { AutoApprovalHandler, checkAutoApproval } from "../auto-approval"
import { MessageManager } from "../message-manager"
import { validateAndFixToolResultIds } from "./validateToolResultIds"
+import { mergeConsecutiveApiMessages } from "./mergeConsecutiveApiMessages"
const MAX_EXPONENTIAL_BACKOFF_SECONDS = 600 // 10 minutes
const DEFAULT_USAGE_COLLECTION_TIMEOUT_MS = 5000 // 5 seconds
@@ -144,11 +140,9 @@ const MAX_CONTEXT_WINDOW_RETRIES = 3 // Maximum retries for context window error
export interface TaskOptions extends CreateTaskOptions {
provider: ClineProvider
apiConfiguration: ProviderSettings
- enableDiff?: boolean
enableCheckpoints?: boolean
checkpointTimeout?: number
enableBridge?: boolean
- fuzzyMatchThreshold?: number
consecutiveMistakeLimit?: number
task?: string
images?: string[]
@@ -208,30 +202,6 @@ export class Task extends EventEmitter implements TaskLike {
*/
private _taskMode: string | undefined
- /**
- * The tool protocol locked to this task. Once set, the task will continue
- * using this protocol even if user settings change.
- *
- * ## Why This Matters
- * When NTC (Native Tool Calling) is enabled, XML parsing does NOT occur.
- * If a task previously used XML tools, resuming it with NTC enabled would
- * break because the tool calls in the history would not be parseable.
- *
- * ## Lifecycle
- *
- * ### For new tasks:
- * 1. Set immediately in constructor via `resolveToolProtocol()`
- * 2. Locked for the lifetime of the task
- *
- * ### For history items:
- * 1. If `historyItem.toolProtocol` exists, use it
- * 2. Otherwise, detect from API history via `detectToolProtocolFromHistory()`
- * 3. If no tools in history, use `resolveToolProtocol()` from current settings
- *
- * @private
- */
- private _taskToolProtocol: ToolProtocol | undefined
-
/**
* Promise that resolves when the task mode has been initialized.
* This ensures async mode initialization completes before the task is used.
@@ -337,8 +307,6 @@ export class Task extends EventEmitter implements TaskLike {
// Editing
diffViewProvider: DiffViewProvider
diffStrategy?: DiffStrategy
- diffEnabled: boolean = false
- fuzzyMatchThreshold: number
didEditFile: boolean = false
// LLM Messages & Chat Messages
@@ -387,7 +355,7 @@ export class Task extends EventEmitter implements TaskLike {
/**
* Push a tool_result block to userMessageContent, preventing duplicates.
- * This is critical for native tool protocol where duplicate tool_use_ids cause API errors.
+ * Duplicate tool_use_ids cause API errors.
*
* @param toolResult - The tool_result block to add
* @returns true if added, false if duplicate was skipped
@@ -410,7 +378,8 @@ export class Task extends EventEmitter implements TaskLike {
didAlreadyUseTool = false
didToolFailInCurrentTurn = false
didCompleteReadingStream = false
- assistantMessageParser?: AssistantMessageParser
+ // No streaming parser is required.
+ assistantMessageParser?: undefined
private providerProfileChangeListener?: (config: { name: string; provider?: string }) => void
// Native tool call streaming state (track which index each tool is at)
@@ -443,11 +412,9 @@ export class Task extends EventEmitter implements TaskLike {
constructor({
provider,
apiConfiguration,
- enableDiff = false,
enableCheckpoints = true,
checkpointTimeout = DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
enableBridge = false,
- fuzzyMatchThreshold = 1.0,
consecutiveMistakeLimit = DEFAULT_CONSECUTIVE_MISTAKE_LIMIT,
task,
images,
@@ -535,8 +502,6 @@ export class Task extends EventEmitter implements TaskLike {
}
}
})
- this.diffEnabled = enableDiff
- this.fuzzyMatchThreshold = fuzzyMatchThreshold
this.consecutiveMistakeLimit = consecutiveMistakeLimit ?? DEFAULT_CONSECUTIVE_MISTAKE_LIMIT
this.providerRef = new WeakRef(provider)
this.globalStoragePath = provider.context.globalStorageUri.fsPath
@@ -558,10 +523,6 @@ export class Task extends EventEmitter implements TaskLike {
this.taskModeReady = Promise.resolve()
this.taskApiConfigReady = Promise.resolve()
TelemetryService.instance.captureTaskRestarted(this.taskId)
-
- // For history items, use the persisted tool protocol if available.
- // If not available (old tasks), it will be detected in resumeTaskFromHistory.
- this._taskToolProtocol = historyItem.toolProtocol
} else {
// For new tasks, don't set the mode/apiConfigName yet - wait for async initialization.
this._taskMode = undefined
@@ -569,26 +530,15 @@ export class Task extends EventEmitter implements TaskLike {
this.taskModeReady = this.initializeTaskMode(provider)
this.taskApiConfigReady = this.initializeTaskApiConfigName(provider)
TelemetryService.instance.captureTaskCreated(this.taskId)
-
- // For new tasks, resolve and lock the tool protocol immediately.
- // This ensures the task will continue using this protocol even if
- // user settings change.
- const modelInfo = this.api.getModel().info
- this._taskToolProtocol = resolveToolProtocol(this.apiConfiguration, modelInfo)
}
- // Initialize the assistant message parser based on the locked tool protocol.
- // For native protocol, tool calls come as tool_call chunks, not XML.
- // For history items without a persisted protocol, we default to XML parser
- // and will update it in resumeTaskFromHistory after detection.
- const effectiveProtocol = this._taskToolProtocol || "xml"
- this.assistantMessageParser = effectiveProtocol !== "native" ? new AssistantMessageParser() : undefined
+ this.assistantMessageParser = undefined
this.messageQueueService = new MessageQueueService()
this.messageQueueStateChangedHandler = () => {
this.emit(RooCodeEventName.TaskUserMessage, this.taskId)
- this.providerRef.deref()?.postStateToWebview()
+ this.providerRef.deref()?.postStateToWebviewWithoutTaskHistory()
}
this.messageQueueService.on("stateChanged", this.messageQueueStateChangedHandler)
@@ -596,23 +546,8 @@ export class Task extends EventEmitter implements TaskLike {
// Listen for provider profile changes to update parser state
this.setupProviderProfileChangeListener(provider)
- // Only set up diff strategy if diff is enabled.
- if (this.diffEnabled) {
- // Default to old strategy, will be updated if experiment is enabled.
- this.diffStrategy = new MultiSearchReplaceDiffStrategy(this.fuzzyMatchThreshold)
-
- // Check experiment asynchronously and update strategy if needed.
- provider.getState().then((state) => {
- const isMultiFileApplyDiffEnabled = experiments.isEnabled(
- state.experiments ?? {},
- EXPERIMENT_IDS.MULTI_FILE_APPLY_DIFF,
- )
-
- if (isMultiFileApplyDiffEnabled) {
- this.diffStrategy = new MultiFileSearchReplaceDiffStrategy(this.fuzzyMatchThreshold)
- }
- })
- }
+ // Set up diff strategy
+ this.diffStrategy = new MultiSearchReplaceDiffStrategy()
this.toolRepetitionDetector = new ToolRepetitionDetector(this.consecutiveMistakeLimit)
@@ -732,8 +667,7 @@ export class Task extends EventEmitter implements TaskLike {
}
/**
- * Sets up a listener for provider profile changes to automatically update the parser state.
- * This ensures the XML/native protocol parser stays synchronized with the current model.
+ * Sets up a listener for provider profile changes.
*
* @private
* @param provider - The ClineProvider instance to listen to
@@ -1061,8 +995,35 @@ export class Task extends EventEmitter implements TaskLike {
this.apiConversationHistory.push(messageWithTs)
} else {
- // For user messages, validate and fix tool_result IDs against the previous assistant message
- const validatedMessage = validateAndFixToolResultIds(message, this.apiConversationHistory)
+ // For user messages, validate tool_result IDs ONLY when the immediately previous *effective* message
+ // is an assistant message.
+ //
+ // If the previous effective message is also a user message (e.g., summary + a new user message),
+ // validating against any earlier assistant message can incorrectly inject placeholder tool_results.
+ const effectiveHistoryForValidation = getEffectiveApiHistory(this.apiConversationHistory)
+ const lastEffective = effectiveHistoryForValidation[effectiveHistoryForValidation.length - 1]
+ const historyForValidation = lastEffective?.role === "assistant" ? effectiveHistoryForValidation : []
+
+ // If the previous effective message is NOT an assistant, convert tool_result blocks to text blocks.
+ // This prevents orphaned tool_results from being filtered out by getEffectiveApiHistory.
+ // This can happen when condensing occurs after the assistant sends tool_uses but before
+ // the user responds - the tool_use blocks get condensed away, leaving orphaned tool_results.
+ let messageToAdd = message
+ if (lastEffective?.role !== "assistant" && Array.isArray(message.content)) {
+ messageToAdd = {
+ ...message,
+ content: message.content.map((block) =>
+ block.type === "tool_result"
+ ? {
+ type: "text" as const,
+ text: `Tool result:\n${typeof block.content === "string" ? block.content : JSON.stringify(block.content)}`,
+ }
+ : block,
+ ),
+ }
+ }
+
+ const validatedMessage = validateAndFixToolResultIds(messageToAdd, historyForValidation)
const messageWithTs = { ...validatedMessage, ts: Date.now() }
this.apiConversationHistory.push(messageWithTs)
}
@@ -1070,6 +1031,10 @@ export class Task extends EventEmitter implements TaskLike {
await this.saveApiConversationHistory()
}
+ // NOTE: We intentionally do NOT mutate stored messages to merge consecutive user turns.
+ // For API requests, consecutive same-role messages are merged via mergeConsecutiveApiMessages()
+ // so rewind/edit behavior can still reference original message boundaries.
+
async overwriteApiConversationHistory(newHistory: ApiMessage[]) {
this.apiConversationHistory = newHistory
await this.saveApiConversationHistory()
@@ -1078,7 +1043,7 @@ export class Task extends EventEmitter implements TaskLike {
/**
* Flush any pending tool results to the API conversation history.
*
- * This is critical for native tool protocol when the task is about to be
+ * This is critical when the task is about to be
* delegated (e.g., via new_task). Before delegation, if other tools were
* called in the same turn before new_task, their tool_result blocks are
* accumulated in `userMessageContent` but haven't been saved to the API
@@ -1102,8 +1067,11 @@ export class Task extends EventEmitter implements TaskLike {
content: this.userMessageContent,
}
- // Validate and fix tool_result IDs against the previous assistant message
- const validatedMessage = validateAndFixToolResultIds(userMessage, this.apiConversationHistory)
+ // Validate and fix tool_result IDs when the previous *effective* message is an assistant message.
+ const effectiveHistoryForValidation = getEffectiveApiHistory(this.apiConversationHistory)
+ const lastEffective = effectiveHistoryForValidation[effectiveHistoryForValidation.length - 1]
+ const historyForValidation = lastEffective?.role === "assistant" ? effectiveHistoryForValidation : []
+ const validatedMessage = validateAndFixToolResultIds(userMessage, historyForValidation)
const userMessageWithTs = { ...validatedMessage, ts: Date.now() }
this.apiConversationHistory.push(userMessageWithTs as ApiMessage)
@@ -1135,7 +1103,9 @@ export class Task extends EventEmitter implements TaskLike {
private async addToClineMessages(message: ClineMessage) {
this.clineMessages.push(message)
const provider = this.providerRef.deref()
- await provider?.postStateToWebview()
+ // Avoid resending large, mostly-static fields (notably taskHistory) on every chat message update.
+ // taskHistory is maintained in-memory in the webview and updated via taskHistoryItemUpdated.
+ await provider?.postStateToWebviewWithoutTaskHistory()
this.emit(RooCodeEventName.Message, { action: "created", message })
await this.saveClineMessages()
@@ -1208,7 +1178,6 @@ export class Task extends EventEmitter implements TaskLike {
mode: this._taskMode || defaultModeSlug, // Use the task's own mode, not the current provider mode.
apiConfigName: this._taskApiConfigName, // Use the task's own provider profile, not the current provider profile.
initialStatus: this.initialStatus,
- toolProtocol: this._taskToolProtocol, // Persist the locked tool protocol.
})
// Emit token/tool usage updates using debounced function
@@ -1539,9 +1508,8 @@ export class Task extends EventEmitter implements TaskLike {
}
/**
- * Updates the API configuration but preserves the locked tool protocol.
- * The task's tool protocol is locked at creation time and should NOT change
- * even when switching between models/profiles with different settings.
+ * Updates the API configuration and rebuilds the API handler.
+ * There is no tool-protocol switching or tool parser swapping.
*
* @param newApiConfiguration - The new API configuration to use
*/
@@ -1549,11 +1517,6 @@ export class Task extends EventEmitter implements TaskLike {
// Update the configuration and rebuild the API handler
this.apiConfiguration = newApiConfiguration
this.api = buildApiHandler(this.apiConfiguration)
-
- // IMPORTANT: Do NOT change the parser based on the new configuration!
- // The task's tool protocol is locked at creation time and must remain
- // consistent throughout the task's lifetime to ensure history can be
- // properly resumed.
}
public async submitUserMessage(
@@ -1590,7 +1553,10 @@ export class Task extends EventEmitter implements TaskLike {
this.emit(RooCodeEventName.TaskUserMessage, this.taskId)
- provider.postMessageToWebview({ type: "invoke", invoke: "sendMessage", text, images })
+ // Handle the message directly instead of routing through the webview.
+ // This avoids a race condition where the webview's message state hasn't
+ // hydrated yet, causing it to interpret the message as a new task request.
+ this.handleWebviewAskResponse("messageResponse", text, images)
} else {
console.error("[Task#submitUserMessage] Provider reference lost")
}
@@ -1616,32 +1582,46 @@ export class Task extends EventEmitter implements TaskLike {
// Get condensing configuration
const state = await this.providerRef.deref()?.getState()
- // These properties may not exist in the state type yet, but are used for condensing configuration
- const customCondensingPrompt = state?.customCondensingPrompt
- const condensingApiConfigId = state?.condensingApiConfigId
- const listApiConfigMeta = state?.listApiConfigMeta
-
- // Determine API handler to use
- let condensingApiHandler: ApiHandler | undefined
- if (condensingApiConfigId && listApiConfigMeta && Array.isArray(listApiConfigMeta)) {
- // Find matching config by ID
- const matchingConfig = listApiConfigMeta.find((config) => config.id === condensingApiConfigId)
- if (matchingConfig) {
- const profile = await this.providerRef.deref()?.providerSettingsManager.getProfile({
- id: condensingApiConfigId,
- })
- // Ensure profile and apiProvider exist before trying to build handler
- if (profile && profile.apiProvider) {
- condensingApiHandler = buildApiHandler(profile)
- }
- }
- }
+ const customCondensingPrompt = state?.customSupportPrompts?.CONDENSE
+ const { mode, apiConfiguration } = state ?? {}
const { contextTokens: prevContextTokens } = this.getTokenUsage()
- // Determine if we're using native tool protocol for proper message handling
- // Use the task's locked protocol, NOT the current settings (fallback to xml if not set)
- const useNativeTools = isNativeProtocol(this._taskToolProtocol ?? "xml")
+ // Build tools for condensing metadata (same tools used for normal API calls)
+ const provider = this.providerRef.deref()
+ let allTools: import("openai").default.Chat.ChatCompletionTool[] = []
+ if (provider) {
+ const modelInfo = this.api.getModel().info
+ const toolsResult = await buildNativeToolsArrayWithRestrictions({
+ provider,
+ cwd: this.cwd,
+ mode,
+ customModes: state?.customModes,
+ experiments: state?.experiments,
+ apiConfiguration,
+ maxReadFileLine: state?.maxReadFileLine ?? -1,
+ maxConcurrentFileReads: state?.maxConcurrentFileReads ?? 5,
+ browserToolEnabled: state?.browserToolEnabled ?? true,
+ modelInfo,
+ includeAllToolsWithRestrictions: false,
+ })
+ allTools = toolsResult.tools
+ }
+
+ // Build metadata with tools and taskId for the condensing API call
+ const metadata: ApiHandlerCreateMessageMetadata = {
+ mode,
+ taskId: this.taskId,
+ ...(allTools.length > 0
+ ? {
+ tools: allTools,
+ tool_choice: "auto",
+ parallelToolCalls: false,
+ }
+ : {}),
+ }
+ // Generate environment details to include in the condensed summary
+ const environmentDetails = await getEnvironmentDetails(this, true)
const {
messages,
@@ -1649,20 +1629,20 @@ export class Task extends EventEmitter implements TaskLike {
cost,
newContextTokens = 0,
error,
+ errorDetails,
condenseId,
} = await summarizeConversation(
this.apiConversationHistory,
this.api, // Main API handler (fallback)
systemPrompt, // Default summarization prompt (fallback)
this.taskId,
- prevContextTokens,
false, // manual trigger
customCondensingPrompt, // User's custom prompt
- condensingApiHandler, // Specific handler for condensing
- useNativeTools, // Pass native tools flag for proper message handling
+ metadata, // Pass metadata with tools
+ environmentDetails, // Include environment details in summary
)
if (error) {
- this.say(
+ await this.say(
"condense_context_error",
error,
undefined /* images */,
@@ -1823,61 +1803,115 @@ export class Task extends EventEmitter implements TaskLike {
relPath ? ` for '${relPath.toPosix()}'` : ""
} without value for required parameter '${paramName}'. Retrying...`,
)
- // Use the task's locked protocol, NOT the current settings (fallback to xml if not set)
- return formatResponse.toolError(
- formatResponse.missingToolParameterError(paramName, this._taskToolProtocol ?? "xml"),
- )
+ return formatResponse.toolError(formatResponse.missingToolParameterError(paramName))
}
// Lifecycle
// Start / Resume / Abort / Dispose
+ /**
+ * Get enabled MCP tools count for this task.
+ * Returns the count along with the number of servers contributing.
+ *
+ * @returns Object with enabledToolCount and enabledServerCount
+ */
+ private async getEnabledMcpToolsCount(): Promise<{ enabledToolCount: number; enabledServerCount: number }> {
+ try {
+ const provider = this.providerRef.deref()
+ if (!provider) {
+ return { enabledToolCount: 0, enabledServerCount: 0 }
+ }
+
+ const { mcpEnabled } = (await provider.getState()) ?? {}
+ if (!(mcpEnabled ?? true)) {
+ return { enabledToolCount: 0, enabledServerCount: 0 }
+ }
+
+ const mcpHub = await McpServerManager.getInstance(provider.context, provider)
+ if (!mcpHub) {
+ return { enabledToolCount: 0, enabledServerCount: 0 }
+ }
+
+ const servers = mcpHub.getServers()
+ return countEnabledMcpTools(servers)
+ } catch (error) {
+ console.error("[Task#getEnabledMcpToolsCount] Error counting MCP tools:", error)
+ return { enabledToolCount: 0, enabledServerCount: 0 }
+ }
+ }
+
private async startTask(task?: string, images?: string[]): Promise {
- if (this.enableBridge) {
- try {
- await BridgeOrchestrator.subscribeToTask(this)
- } catch (error) {
- console.error(
- `[Task#startTask] BridgeOrchestrator.subscribeToTask() failed: ${error instanceof Error ? error.message : String(error)}`,
+ try {
+ if (this.enableBridge) {
+ try {
+ await BridgeOrchestrator.subscribeToTask(this)
+ } catch (error) {
+ console.error(
+ `[Task#startTask] BridgeOrchestrator.subscribeToTask() failed: ${error instanceof Error ? error.message : String(error)}`,
+ )
+ }
+ }
+
+ // `conversationHistory` (for API) and `clineMessages` (for webview)
+ // need to be in sync.
+ // If the extension process were killed, then on restart the
+ // `clineMessages` might not be empty, so we need to set it to [] when
+ // we create a new Cline client (otherwise webview would show stale
+ // messages from previous session).
+ this.clineMessages = []
+ this.apiConversationHistory = []
+
+ // The todo list is already set in the constructor if initialTodos were provided
+ // No need to add any messages - the todoList property is already set
+
+ await this.providerRef.deref()?.postStateToWebviewWithoutTaskHistory()
+
+ await this.say("text", task, images)
+
+ // Check for too many MCP tools and warn the user
+ const { enabledToolCount, enabledServerCount } = await this.getEnabledMcpToolsCount()
+ if (enabledToolCount > MAX_MCP_TOOLS_THRESHOLD) {
+ await this.say(
+ "too_many_tools_warning",
+ JSON.stringify({
+ toolCount: enabledToolCount,
+ serverCount: enabledServerCount,
+ threshold: MAX_MCP_TOOLS_THRESHOLD,
+ }),
+ undefined,
+ undefined,
+ undefined,
+ undefined,
+ { isNonInteractive: true },
)
}
- }
+ this.isInitialized = true
- // `conversationHistory` (for API) and `clineMessages` (for webview)
- // need to be in sync.
- // If the extension process were killed, then on restart the
- // `clineMessages` might not be empty, so we need to set it to [] when
- // we create a new Cline client (otherwise webview would show stale
- // messages from previous session).
- this.clineMessages = []
- this.apiConversationHistory = []
+ const imageBlocks: Anthropic.ImageBlockParam[] = formatResponse.imageBlocks(images)
- // The todo list is already set in the constructor if initialTodos were provided
- // No need to add any messages - the todoList property is already set
-
- await this.providerRef.deref()?.postStateToWebview()
-
- await this.say("text", task, images)
- this.isInitialized = true
-
- let imageBlocks: Anthropic.ImageBlockParam[] = formatResponse.imageBlocks(images)
-
- // Task starting
-
- await this.initiateTaskLoop([
- {
- type: "text",
- text: `\n${task}\n`,
- },
- ...imageBlocks,
- ]).catch((error) => {
- // Swallow loop rejection when the task was intentionally abandoned/aborted
- // during delegation or user cancellation to prevent unhandled rejections.
- if (this.abandoned === true || this.abortReason === "user_cancelled") {
+ // Task starting
+ await this.initiateTaskLoop([
+ {
+ type: "text",
+ text: `\n${task}\n`,
+ },
+ ...imageBlocks,
+ ]).catch((error) => {
+ // Swallow loop rejection when the task was intentionally abandoned/aborted
+ // during delegation or user cancellation to prevent unhandled rejections.
+ if (this.abandoned === true || this.abortReason === "user_cancelled") {
+ return
+ }
+ throw error
+ })
+ } catch (error) {
+ // In tests and some UX flows, tasks can be aborted while `startTask` is still
+ // initializing. Treat abort/abandon as expected and avoid unhandled rejections.
+ if (this.abandoned === true || this.abort === true || this.abortReason === "user_cancelled") {
return
}
throw error
- })
+ }
}
private async resumeTaskFromHistory() {
@@ -1942,31 +1976,6 @@ export class Task extends EventEmitter implements TaskLike {
// the task first.
this.apiConversationHistory = await this.getSavedApiConversationHistory()
- // If we don't have a persisted tool protocol (old tasks before this feature),
- // detect it from the API history. This ensures tasks that previously used
- // XML tools will continue using XML even if NTC is now enabled.
- if (!this._taskToolProtocol) {
- const detectedProtocol = detectToolProtocolFromHistory(this.apiConversationHistory)
- if (detectedProtocol) {
- // Found tool calls in history - lock to that protocol
- this._taskToolProtocol = detectedProtocol
- } else {
- // No tool calls in history yet - use current settings
- const modelInfo = this.api.getModel().info
- this._taskToolProtocol = resolveToolProtocol(this.apiConfiguration, modelInfo)
- }
-
- // Update parser state to match the detected/resolved protocol
- const shouldUseXmlParser = this._taskToolProtocol === "xml"
- if (shouldUseXmlParser && !this.assistantMessageParser) {
- this.assistantMessageParser = new AssistantMessageParser()
- } else if (!shouldUseXmlParser && this.assistantMessageParser) {
- this.assistantMessageParser.reset()
- this.assistantMessageParser = undefined
- }
- } else {
- }
-
const lastClineMessage = this.clineMessages
.slice()
.reverse()
@@ -1996,50 +2005,7 @@ export class Task extends EventEmitter implements TaskLike {
// even if it goes out of sync with cline messages.
let existingApiConversationHistory: ApiMessage[] = await this.getSavedApiConversationHistory()
- // v2.0 xml tags refactor caveat: since we don't use tools anymore for XML protocol,
- // we need to replace all tool use blocks with a text block since the API disallows
- // conversations with tool uses and no tool schema.
- // For native protocol, we preserve tool_use and tool_result blocks as they're expected by the API.
- // IMPORTANT: Use the task's locked protocol, NOT the current settings!
- const useNative = isNativeProtocol(this._taskToolProtocol)
-
- // Only convert tool blocks to text for XML protocol
- // For native protocol, the API expects proper tool_use/tool_result structure
- if (!useNative) {
- const conversationWithoutToolBlocks = existingApiConversationHistory.map((message) => {
- if (Array.isArray(message.content)) {
- const newContent = message.content.map((block) => {
- if (block.type === "tool_use") {
- // Format tool invocation based on the task's locked protocol
- const params = block.input as Record
- const formattedText = formatToolInvocation(block.name, params, this._taskToolProtocol)
-
- return {
- type: "text",
- text: formattedText,
- } as Anthropic.Messages.TextBlockParam
- } else if (block.type === "tool_result") {
- // Convert block.content to text block array, removing images
- const contentAsTextBlocks = Array.isArray(block.content)
- ? block.content.filter((item) => item.type === "text")
- : [{ type: "text", text: block.content }]
- const textContent = contentAsTextBlocks.map((item) => item.text).join("\n\n")
- const toolName = findToolName(block.tool_use_id, existingApiConversationHistory)
- return {
- type: "text",
- text: `[${toolName} Result]\n\n${textContent}`,
- } as Anthropic.Messages.TextBlockParam
- }
- return block
- })
- return { ...message, content: newContent }
- }
- return message
- })
- existingApiConversationHistory = conversationWithoutToolBlocks
- }
-
- // FIXME: remove tool use blocks altogether
+ // Tool blocks are always preserved; native tool calling only.
// if the last message is an assistant message, we need to check if there's tool use since every tool use has to have a tool response
// if there's no tool use and only a text block, then we can just add a user message
@@ -2146,7 +2112,7 @@ export class Task extends EventEmitter implements TaskLike {
if (responseText) {
newUserContent.push({
type: "text",
- text: `\n\nNew instructions for task continuation:\n