Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/types/src/experiment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const experimentIds = [
"runSlashCommand",
"multipleNativeToolCalls",
"customTools",
"unifiedUserMessageTag",
] as const

export const experimentIdsSchema = z.enum(experimentIds)
Expand All @@ -32,6 +33,7 @@ export const experimentsSchema = z.object({
runSlashCommand: z.boolean().optional(),
multipleNativeToolCalls: z.boolean().optional(),
customTools: z.boolean().optional(),
unifiedUserMessageTag: z.boolean().optional(),
})

export type Experiments = z.infer<typeof experimentsSchema>
Expand Down
15 changes: 10 additions & 5 deletions src/core/assistant-message/presentAssistantMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ export async function presentAssistantMessage(cline: Task) {
cline.presentAssistantMessageLocked = true
cline.presentAssistantMessageHasPendingUpdates = false

// Get experiment setting for unified tags once for this presentation
const state = await cline.providerRef.deref()?.getState()
const useUnifiedTag = experiments.isEnabled(state?.experiments ?? {}, EXPERIMENT_IDS.UNIFIED_USER_MESSAGE_TAG)

if (cline.currentStreamingContentIndex >= cline.assistantMessageContent.length) {
// This may happen if the last content block was completed before
// streaming could finish. If streaming is finished, and we're out of
Expand Down Expand Up @@ -174,7 +178,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, toolProtocol, useUnifiedTag)
resultContent = `${feedbackText}\n\n${resultContent}`

// Add feedback images to the image blocks
Expand Down Expand Up @@ -221,7 +225,7 @@ export async function presentAssistantMessage(cline: Task) {
await cline.say("user_feedback", text, images)
pushToolResult(
formatResponse.toolResult(
formatResponse.toolDeniedWithFeedback(text, toolProtocol),
formatResponse.toolDeniedWithFeedback(text, toolProtocol, useUnifiedTag),
images,
),
)
Expand Down Expand Up @@ -372,8 +376,7 @@ export async function presentAssistantMessage(cline: Task) {
break
}
case "tool_use": {
// Fetch state early so it's available for toolDescription and validation
const state = await cline.providerRef.deref()?.getState()
// Use already-fetched state for toolDescription and validation
const { mode, customModes, experiments: stateExperiments } = state ?? {}

const toolDescription = (): string => {
Expand Down Expand Up @@ -556,6 +559,7 @@ export async function presentAssistantMessage(cline: Task) {
const feedbackText = formatResponse.toolApprovedWithFeedback(
approvalFeedback.text,
toolProtocol,
useUnifiedTag,
)
resultContent = `${feedbackText}\n\n${resultContent}`

Expand Down Expand Up @@ -597,6 +601,7 @@ export async function presentAssistantMessage(cline: Task) {
const feedbackText = formatResponse.toolApprovedWithFeedback(
approvalFeedback.text,
toolProtocol,
useUnifiedTag,
)
resultContent = `${feedbackText}\n\n${resultContent}`
}
Expand Down Expand Up @@ -658,7 +663,7 @@ export async function presentAssistantMessage(cline: Task) {
await cline.say("user_feedback", text, images)
pushToolResult(
formatResponse.toolResult(
formatResponse.toolDeniedWithFeedback(text, toolProtocol),
formatResponse.toolDeniedWithFeedback(text, toolProtocol, useUnifiedTag),
images,
),
)
Expand Down
22 changes: 9 additions & 13 deletions src/core/mentions/processUserContentMentions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { Anthropic } from "@anthropic-ai/sdk"
import { parseMentions, ParseMentionsResult } from "./index"
import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
import { FileContextTracker } from "../context-tracking/FileContextTracker"
import { hasUserContentTags } from "../../utils/userContentTags"

export interface ProcessUserContentMentionsResult {
content: Anthropic.Messages.ContentBlockParam[]
mode?: string // Mode from the first slash command that has one
}

/**
* Process mentions in user content, specifically within task and feedback tags
* Process mentions in user content, specifically within user message tags
*/
export async function processUserContentMentions({
userContent,
Expand All @@ -21,6 +22,7 @@ export async function processUserContentMentions({
includeDiagnosticMessages = true,
maxDiagnosticMessages = 50,
maxReadFileLine,
useUnifiedTag = false,
}: {
userContent: Anthropic.Messages.ContentBlockParam[]
cwd: string
Expand All @@ -31,27 +33,21 @@ export async function processUserContentMentions({
includeDiagnosticMessages?: boolean
maxDiagnosticMessages?: number
maxReadFileLine?: number
useUnifiedTag?: boolean
}): Promise<ProcessUserContentMentionsResult> {
// Track the first mode found from slash commands
let commandMode: string | undefined

// 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)
// 2. ToolResultBlockParam's content/context text arrays if it contains
// "<feedback>" (see formatToolDeniedFeedback, attemptCompletion,
// executeCommand, and consecutiveMistakeCount >= 3) or "<answer>"
// (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).
// 1. All TextBlockParam's text (first user message)
// 2. ToolResultBlockParam's content/context text arrays if it contains user content tags
// 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("<task>") ||
text.includes("<feedback>") ||
text.includes("<answer>") ||
text.includes("<user_message>")
const shouldProcessMentions = (text: string) => hasUserContentTags(text, useUnifiedTag)

if (block.type === "text") {
if (shouldProcessMentions(block.text)) {
Expand Down
19 changes: 10 additions & 9 deletions src/core/prompts/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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"
import { wrapUserContent } from "../../utils/userContentTags"

export const formatResponse = {
toolDenied: (protocol?: ToolProtocol) => {
Expand All @@ -16,26 +17,26 @@ export const formatResponse = {
return `The user denied this operation.`
},

toolDeniedWithFeedback: (feedback?: string, protocol?: ToolProtocol) => {
toolDeniedWithFeedback: (feedback?: string, protocol?: ToolProtocol, useUnifiedTag?: boolean) => {
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<feedback>\n${feedback}\n</feedback>`
const wrappedFeedback = wrapUserContent(feedback ?? "", "feedback", useUnifiedTag ?? false)
return `The user denied this operation and responded with the message:\n${wrappedFeedback}`
},

toolApprovedWithFeedback: (feedback?: string, protocol?: ToolProtocol) => {
toolApprovedWithFeedback: (feedback?: string, protocol?: ToolProtocol, useUnifiedTag?: boolean) => {
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<feedback>\n${feedback}\n</feedback>`
const wrappedFeedback = wrapUserContent(feedback ?? "", "feedback", useUnifiedTag ?? false)
return `The user approved this operation and responded with the message:\n${wrappedFeedback}`
},

toolError: (error?: string, protocol?: ToolProtocol) => {
Expand Down Expand Up @@ -77,15 +78,15 @@ 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) => {
tooManyMistakes: (feedback?: string, protocol?: ToolProtocol, useUnifiedTag?: boolean) => {
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<feedback>\n${feedback}\n</feedback>`
const wrappedFeedback = wrapUserContent(feedback ?? "", "feedback", useUnifiedTag ?? false)
return `You seem to be having trouble proceeding. The user has provided the following feedback to help guide you:\n${wrappedFeedback}`
},

missingToolParameterError: (paramName: string, protocol?: ToolProtocol) => {
Expand Down
11 changes: 10 additions & 1 deletion src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ 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"
import { wrapUserContent } from "../../utils/userContentTags"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wrapUserContent is imported here but never used in this file. The initial task message in startTask() around line 1867 still uses hardcoded <task> tags (text: \\n${task}\n`) instead of calling this utility. If the experiment flag is enabled, task start messages will still emit rather than<user_message>`, which contradicts the PR description's claim that task start messages are conditionally wrapped.

Fix it with Roo Code or mention @roomote and request a fix.


// services
import { UrlContentFetcher } from "../../services/browser/UrlContentFetcher"
Expand Down Expand Up @@ -2508,9 +2509,13 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
)

if (response === "messageResponse") {
// Get experiment setting for unified tags
const mistakeState = await this.providerRef.deref()?.getState()
const useUnifiedTagForMistakes = experiments.isEnabled(mistakeState?.experiments ?? {}, EXPERIMENT_IDS.UNIFIED_USER_MESSAGE_TAG)

currentUserContent.push(
...[
{ type: "text" as const, text: formatResponse.tooManyMistakes(text) },
{ type: "text" as const, text: formatResponse.tooManyMistakes(text, this._taskToolProtocol, useUnifiedTagForMistakes) },
...formatResponse.imageBlocks(images),
],
)
Expand Down Expand Up @@ -2553,8 +2558,11 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
includeDiagnosticMessages = true,
maxDiagnosticMessages = 50,
maxReadFileLine = -1,
experiments: experimentsConfig,
} = (await this.providerRef.deref()?.getState()) ?? {}

const useUnifiedTag = experiments.isEnabled(experimentsConfig ?? {}, EXPERIMENT_IDS.UNIFIED_USER_MESSAGE_TAG)

const { content: parsedUserContent, mode: slashCommandMode } = await processUserContentMentions({
userContent: currentUserContent,
cwd: this.cwd,
Expand All @@ -2565,6 +2573,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
includeDiagnosticMessages,
maxDiagnosticMessages,
maxReadFileLine,
useUnifiedTag,
})

// Switch mode if specified in a slash command's frontmatter
Expand Down
10 changes: 9 additions & 1 deletion src/core/tools/AskFollowupQuestionTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Task } from "../task/Task"
import { formatResponse } from "../prompts/responses"
import { parseXml } from "../../utils/xml"
import type { ToolUse } from "../../shared/tools"
import { wrapUserContent } from "../../utils/userContentTags"
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"

import { BaseTool, ToolCallbacks } from "./BaseTool"

Expand Down Expand Up @@ -86,7 +88,13 @@ export class AskFollowupQuestionTool extends BaseTool<"ask_followup_question"> {
task.consecutiveMistakeCount = 0
const { text, images } = await task.ask("followup", JSON.stringify(follow_up_json), false)
await task.say("user_feedback", text ?? "", images)
pushToolResult(formatResponse.toolResult(`<answer>\n${text}\n</answer>`, images))

// Get experiment setting for unified tags
const state = await task.providerRef.deref()?.getState()
const useUnifiedTag = experiments.isEnabled(state?.experiments ?? {}, EXPERIMENT_IDS.UNIFIED_USER_MESSAGE_TAG)
const wrappedAnswer = wrapUserContent(text ?? "", "answer", useUnifiedTag)

pushToolResult(formatResponse.toolResult(wrappedAnswer, images))
} catch (error) {
await handleError("asking question", error as Error)
}
Expand Down
10 changes: 8 additions & 2 deletions src/core/tools/AttemptCompletionTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { formatResponse } from "../prompts/responses"
import { Package } from "../../shared/package"
import type { ToolUse } from "../../shared/tools"
import { t } from "../../i18n"
import { wrapUserContent } from "../../utils/userContentTags"
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"

import { BaseTool, ToolCallbacks } from "./BaseTool"

Expand Down Expand Up @@ -150,8 +152,12 @@ export class AttemptCompletionTool extends BaseTool<"attempt_completion"> {
// User provided feedback - push tool result to continue the conversation
await task.say("user_feedback", text ?? "", images)

const feedbackText = `The user has provided feedback on the results. Consider their input to continue the task, and then attempt completion again.\n<feedback>\n${text}\n</feedback>`
pushToolResult(formatResponse.toolResult(feedbackText, images))
// Get experiment setting for unified tags
const state = await task.providerRef.deref()?.getState()
const useUnifiedTag = experiments.isEnabled(state?.experiments ?? {}, EXPERIMENT_IDS.UNIFIED_USER_MESSAGE_TAG)
const wrappedFeedback = wrapUserContent(text ?? "", "feedback", useUnifiedTag)

pushToolResult(formatResponse.toolResult(wrappedFeedback, images))
} catch (error) {
await handleError("inspecting site", error as Error)
}
Expand Down
10 changes: 8 additions & 2 deletions src/core/tools/ExecuteCommandTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { Terminal } from "../../integrations/terminal/Terminal"
import { Package } from "../../shared/package"
import { t } from "../../i18n"
import { BaseTool, ToolCallbacks } from "./BaseTool"
import { wrapUserContent } from "../../utils/userContentTags"
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"

class ShellIntegrationError extends Error {}

Expand Down Expand Up @@ -334,14 +336,18 @@ export async function executeCommandInTerminal(
const { text, images } = message
await task.say("user_feedback", text, images)

// Get experiment setting for unified tags
const state = await task.providerRef.deref()?.getState()
const useUnifiedTag = experiments.isEnabled(state?.experiments ?? {}, EXPERIMENT_IDS.UNIFIED_USER_MESSAGE_TAG)
const wrappedFeedback = wrapUserContent(text ?? "", "feedback", useUnifiedTag)

return [
true,
formatResponse.toolResult(
[
`Command is still running in terminal from '${terminal.getCurrentWorkingDirectory().toPosix()}'.`,
result.length > 0 ? `Here's the output so far:\n${result}\n` : "\n",
`The user provided the following feedback:`,
`<feedback>\n${text}\n</feedback>`,
wrappedFeedback,
].join("\n"),
images,
),
Expand Down
9 changes: 6 additions & 3 deletions src/core/tools/ReadFileTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { parseSourceCodeDefinitionsForFile } from "../../services/tree-sitter"
import { parseXml } from "../../utils/xml"
import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
import type { ToolUse } from "../../shared/tools"
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"

import {
DEFAULT_MAX_IMAGE_FILE_SIZE_MB,
Expand Down Expand Up @@ -343,7 +344,9 @@ export class ReadFileTool extends BaseTool<"read_file"> {
maxReadFileLine = -1,
maxImageFileSize = DEFAULT_MAX_IMAGE_FILE_SIZE_MB,
maxTotalImageSize = DEFAULT_MAX_TOTAL_IMAGE_SIZE_MB,
experiments: stateExperiments,
} = state ?? {}
const useUnifiedTag = experiments.isEnabled(stateExperiments ?? {}, EXPERIMENT_IDS.UNIFIED_USER_MESSAGE_TAG)

for (const fileResult of fileResults) {
if (fileResult.status !== "approved") continue
Expand Down Expand Up @@ -649,17 +652,17 @@ export class ReadFileTool extends BaseTool<"read_file"> {
const deniedWithFeedback = fileResults.find((result) => result.status === "denied" && result.feedbackText)

if (deniedWithFeedback && deniedWithFeedback.feedbackText) {
statusMessage = formatResponse.toolDeniedWithFeedback(deniedWithFeedback.feedbackText)
statusMessage = formatResponse.toolDeniedWithFeedback(deniedWithFeedback.feedbackText, toolProtocol, useUnifiedTag)
feedbackImages = deniedWithFeedback.feedbackImages || []
} else if (task.didRejectTool) {
statusMessage = formatResponse.toolDenied()
statusMessage = formatResponse.toolDenied(toolProtocol)
} else {
const approvedWithFeedback = fileResults.find(
(result) => result.status === "approved" && result.feedbackText,
)

if (approvedWithFeedback && approvedWithFeedback.feedbackText) {
statusMessage = formatResponse.toolApprovedWithFeedback(approvedWithFeedback.feedbackText)
statusMessage = formatResponse.toolApprovedWithFeedback(approvedWithFeedback.feedbackText, toolProtocol, useUnifiedTag)
feedbackImages = approvedWithFeedback.feedbackImages || []
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/shared/experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const EXPERIMENT_IDS = {
RUN_SLASH_COMMAND: "runSlashCommand",
MULTIPLE_NATIVE_TOOL_CALLS: "multipleNativeToolCalls",
CUSTOM_TOOLS: "customTools",
UNIFIED_USER_MESSAGE_TAG: "unifiedUserMessageTag",
} as const satisfies Record<string, ExperimentId>

type _AssertExperimentIds = AssertEqual<Equals<ExperimentId, Values<typeof EXPERIMENT_IDS>>>
Expand All @@ -26,6 +27,7 @@ export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
RUN_SLASH_COMMAND: { enabled: false },
MULTIPLE_NATIVE_TOOL_CALLS: { enabled: false },
CUSTOM_TOOLS: { enabled: false },
UNIFIED_USER_MESSAGE_TAG: { enabled: false },
}

export const experimentDefault = Object.fromEntries(
Expand Down
Loading
Loading