refactor: unify user content tags to <user_message>

Replace all user content wrapper tags (<task>, <feedback>, <answer>, <user_message>)
with a single unified <user_message> tag and strip all accompanying explanatory blurbs.

Changes:
- processUserContentMentions.ts: Simplified shouldProcessMentions() to only detect <user_message>
- responses.ts: Updated toolDeniedWithFeedback(), toolApprovedWithFeedback(), tooManyMistakes()
  - Removed explanatory blurbs from XML output
  - Removed 'message' field from native protocol JSON
- Task.ts: Changed initial task and continuation wrappers to use <user_message>
- AttemptCompletionTool.ts: Removed blurb, changed <feedback> to <user_message>
- AskFollowupQuestionTool.ts: Changed <answer> to <user_message>
- Updated all related test files

Breaking changes:
- Old task history with <task>, <feedback>, or <answer> tags will no longer have mentions re-parsed
- Models no longer receive explicit blurbs explaining the nature of user input
This commit is contained in:
Hannes Rudolph 2025-12-03 21:41:09 -07:00
parent 72a6805b4b
commit 0d013e35c4
8 changed files with 38 additions and 67 deletions

View file

@ -31,7 +31,7 @@ describe("processUserContentMentions", () => {
const userContent = [
{
type: "text" as const,
text: "<task>Read file with limit</task>",
text: "<user_message>Read file with limit</user_message>",
},
]
@ -45,7 +45,7 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).toHaveBeenCalledWith(
"<task>Read file with limit</task>",
"<user_message>Read file with limit</user_message>",
"/test",
mockUrlContentFetcher,
mockFileContextTracker,
@ -61,7 +61,7 @@ describe("processUserContentMentions", () => {
const userContent = [
{
type: "text" as const,
text: "<task>Read file without limit</task>",
text: "<user_message>Read file without limit</user_message>",
},
]
@ -74,7 +74,7 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).toHaveBeenCalledWith(
"<task>Read file without limit</task>",
"<user_message>Read file without limit</user_message>",
"/test",
mockUrlContentFetcher,
mockFileContextTracker,
@ -90,7 +90,7 @@ describe("processUserContentMentions", () => {
const userContent = [
{
type: "text" as const,
text: "<task>Read unlimited lines</task>",
text: "<user_message>Read unlimited lines</user_message>",
},
]
@ -104,7 +104,7 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).toHaveBeenCalledWith(
"<task>Read unlimited lines</task>",
"<user_message>Read unlimited lines</user_message>",
"/test",
mockUrlContentFetcher,
mockFileContextTracker,
@ -118,11 +118,11 @@ describe("processUserContentMentions", () => {
})
describe("content processing", () => {
it("should process text blocks with <task> tags", async () => {
it("should process text blocks with <user_message> tags", async () => {
const userContent = [
{
type: "text" as const,
text: "<task>Do something</task>",
text: "<user_message>Do something</user_message>",
},
]
@ -136,33 +136,11 @@ describe("processUserContentMentions", () => {
expect(parseMentions).toHaveBeenCalled()
expect(result[0]).toEqual({
type: "text",
text: "parsed: <task>Do something</task>",
text: "parsed: <user_message>Do something</user_message>",
})
})
it("should process text blocks with <feedback> tags", async () => {
const userContent = [
{
type: "text" as const,
text: "<feedback>Fix this issue</feedback>",
},
]
const result = await processUserContentMentions({
userContent,
cwd: "/test",
urlContentFetcher: mockUrlContentFetcher,
fileContextTracker: mockFileContextTracker,
})
expect(parseMentions).toHaveBeenCalled()
expect(result[0]).toEqual({
type: "text",
text: "parsed: <feedback>Fix this issue</feedback>",
})
})
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,
@ -186,7 +164,7 @@ describe("processUserContentMentions", () => {
{
type: "tool_result" as const,
tool_use_id: "123",
content: "<feedback>Tool feedback</feedback>",
content: "<user_message>Tool feedback</user_message>",
},
]
@ -201,7 +179,7 @@ describe("processUserContentMentions", () => {
expect(result[0]).toEqual({
type: "tool_result",
tool_use_id: "123",
content: "parsed: <feedback>Tool feedback</feedback>",
content: "parsed: <user_message>Tool feedback</user_message>",
})
})
@ -213,7 +191,7 @@ describe("processUserContentMentions", () => {
content: [
{
type: "text" as const,
text: "<task>Array task</task>",
text: "<user_message>Array task</user_message>",
},
{
type: "text" as const,
@ -237,7 +215,7 @@ describe("processUserContentMentions", () => {
content: [
{
type: "text",
text: "parsed: <task>Array task</task>",
text: "parsed: <user_message>Array task</user_message>",
},
{
type: "text",
@ -251,7 +229,7 @@ describe("processUserContentMentions", () => {
const userContent = [
{
type: "text" as const,
text: "<task>First task</task>",
text: "<user_message>First task</user_message>",
},
{
type: "image" as const,
@ -264,7 +242,7 @@ describe("processUserContentMentions", () => {
{
type: "tool_result" as const,
tool_use_id: "456",
content: "<feedback>Feedback</feedback>",
content: "<user_message>Feedback</user_message>",
},
]
@ -280,13 +258,13 @@ describe("processUserContentMentions", () => {
expect(result).toHaveLength(3)
expect(result[0]).toEqual({
type: "text",
text: "parsed: <task>First task</task>",
text: "parsed: <user_message>First task</user_message>",
})
expect(result[1]).toEqual(userContent[1]) // Image block unchanged
expect(result[2]).toEqual({
type: "tool_result",
tool_use_id: "456",
content: "parsed: <feedback>Feedback</feedback>",
content: "parsed: <user_message>Feedback</user_message>",
})
})
})
@ -296,7 +274,7 @@ describe("processUserContentMentions", () => {
const userContent = [
{
type: "text" as const,
text: "<task>Test default</task>",
text: "<user_message>Test default</user_message>",
},
]
@ -308,7 +286,7 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).toHaveBeenCalledWith(
"<task>Test default</task>",
"<user_message>Test default</user_message>",
"/test",
mockUrlContentFetcher,
mockFileContextTracker,
@ -324,7 +302,7 @@ describe("processUserContentMentions", () => {
const userContent = [
{
type: "text" as const,
text: "<task>Test explicit false</task>",
text: "<user_message>Test explicit false</user_message>",
},
]
@ -337,7 +315,7 @@ describe("processUserContentMentions", () => {
})
expect(parseMentions).toHaveBeenCalledWith(
"<task>Test explicit false</task>",
"<user_message>Test explicit false</user_message>",
"/test",
mockUrlContentFetcher,
mockFileContextTracker,

View file

@ -39,11 +39,7 @@ export async function processUserContentMentions({
// should parse mentions).
return 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) => text.includes("<user_message>")
if (block.type === "text") {
if (shouldProcessMentions(block.text)) {

View file

@ -20,22 +20,20 @@ export const formatResponse = {
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>`
return `<user_message>\n${feedback}\n</user_message>`
},
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<feedback>\n${feedback}\n</feedback>`
return `<user_message>\n${feedback}\n</user_message>`
},
toolError: (error?: string, protocol?: ToolProtocol) => {
@ -81,11 +79,10 @@ Otherwise, if you have not completed the task and do not need additional informa
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>`
return `<user_message>\n${feedback}\n</user_message>`
},
missingToolParameterError: (paramName: string, protocol?: ToolProtocol) => {

View file

@ -1546,7 +1546,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
await this.initiateTaskLoop([
{
type: "text",
text: `<task>\n${task}\n</task>`,
text: `<user_message>\n${task}\n</user_message>`,
},
...imageBlocks,
]).catch((error) => {
@ -1801,7 +1801,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
if (responseText) {
newUserContent.push({
type: "text",
text: `\n\nNew instructions for task continuation:\n<user_message>\n${responseText}\n</user_message>`,
text: `<user_message>\n${responseText}\n</user_message>`,
})
}

View file

@ -881,7 +881,7 @@ describe("Cline", () => {
})
describe("processUserContentMentions", () => {
it("should process mentions in task and feedback tags", async () => {
it("should process mentions in user_message tags", async () => {
const [cline, task] = Task.create({
provider: mockProvider,
apiConfiguration: mockApiConfig,
@ -895,7 +895,7 @@ describe("Cline", () => {
} as const,
{
type: "text",
text: "<task>Text with 'some/path' (see below for file content) in task tags</task>",
text: "<user_message>Text with 'some/path' (see below for file content) in user_message tags</user_message>",
} as const,
{
type: "tool_result",
@ -903,7 +903,7 @@ describe("Cline", () => {
content: [
{
type: "text",
text: "<feedback>Check 'some/path' (see below for file content)</feedback>",
text: "<user_message>Check 'some/path' (see below for file content)</user_message>",
},
],
} as Anthropic.ToolResultBlockParam,
@ -931,18 +931,18 @@ describe("Cline", () => {
"Regular text with 'some/path' (see below for file content)",
)
// Text within task tags should be processed
// Text within user_message tags should be processed
expect((processedContent[1] as Anthropic.TextBlockParam).text).toContain("processed:")
expect((processedContent[1] as Anthropic.TextBlockParam).text).toContain(
"<task>Text with 'some/path' (see below for file content) in task tags</task>",
"<user_message>Text with 'some/path' (see below for file content) in user_message tags</user_message>",
)
// Feedback tag content should be processed
// user_message tag content should be processed
const toolResult1 = processedContent[2] as Anthropic.ToolResultBlockParam
const content1 = Array.isArray(toolResult1.content) ? toolResult1.content[0] : toolResult1.content
expect((content1 as Anthropic.TextBlockParam).text).toContain("processed:")
expect((content1 as Anthropic.TextBlockParam).text).toContain(
"<feedback>Check 'some/path' (see below for file content)</feedback>",
"<user_message>Check 'some/path' (see below for file content)</user_message>",
)
// Regular tool result should not be processed

View file

@ -292,7 +292,7 @@ describe("Task Tool History Handling", () => {
},
{
type: "text" as const,
text: "Another message with <task> tags",
text: "Another message with <user_message> tags",
},
{
type: "tool_result" as const,

View file

@ -85,7 +85,7 @@ 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))
pushToolResult(formatResponse.toolResult(`<user_message>\n${text}\n</user_message>`, images))
} catch (error) {
await handleError("asking question", error as Error)
}

View file

@ -143,7 +143,7 @@ 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>`
const feedbackText = `<user_message>\n${text}\n</user_message>`
pushToolResult(formatResponse.toolResult(feedbackText, images))
} catch (error) {
await handleError("inspecting site", error as Error)