Skip to content

Commit 5588403

Browse files
roomoteMerge Resolver
authored andcommitted
feat: update ask_followup_question format to use nested XML elements
- Changed from attribute-based format (<suggest mode="architect">) to nested element format (<suggest><mode>architect</mode><content>...</content></suggest>) - Updated documentation in ask-followup-question.ts with new format and examples - Modified parsing logic in askFollowupQuestionTool.ts to handle new format while maintaining backward compatibility - Added comprehensive tests for both new format and backward compatibility - All existing tests pass
1 parent 613abe0 commit 5588403

File tree

3 files changed

+158
-20
lines changed

3 files changed

+158
-20
lines changed

src/core/prompts/tools/ask-followup-question.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,53 @@ Description: Ask the user a question to gather additional information needed to
44
55
Parameters:
66
- question: (required) A clear, specific question addressing the information needed
7-
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag. Suggestions must be complete, actionable answers without placeholders. Optionally include mode attribute to switch modes (code/architect/etc.)
7+
- follow_up: (required) A list of 2-4 suggested answers, each in its own <suggest> tag with nested <content> element. Suggestions must be complete, actionable answers without placeholders. Optionally include <mode> element to switch modes (code/architect/etc.)
88
99
Usage:
1010
<ask_followup_question>
1111
<question>Your question here</question>
1212
<follow_up>
13-
<suggest>First suggestion</suggest>
14-
<suggest mode="code">Action with mode switch</suggest>
13+
<suggest>
14+
<content>Your suggested answer here</content>
15+
</suggest>
16+
<suggest>
17+
<mode>code</mode>
18+
<content>Implement the solution</content>
19+
</suggest>
1520
</follow_up>
1621
</ask_followup_question>
1722
1823
Example:
1924
<ask_followup_question>
2025
<question>What is the path to the frontend-config.json file?</question>
2126
<follow_up>
22-
<suggest>./src/frontend-config.json</suggest>
23-
<suggest>./config/frontend-config.json</suggest>
24-
<suggest>./frontend-config.json</suggest>
27+
<suggest>
28+
<content>./src/frontend-config.json</content>
29+
</suggest>
30+
<suggest>
31+
<content>./config/frontend-config.json</content>
32+
</suggest>
33+
<suggest>
34+
<content>./frontend-config.json</content>
35+
</suggest>
36+
</follow_up>
37+
</ask_followup_question>
38+
39+
Example: Asking a question with mode switching options
40+
<ask_followup_question>
41+
<question>How would you like to proceed with this task?</question>
42+
<follow_up>
43+
<suggest>
44+
<mode>code</mode>
45+
<content>Start implementing the solution</content>
46+
</suggest>
47+
<suggest>
48+
<mode>architect</mode>
49+
<content>Plan the architecture first</content>
50+
</suggest>
51+
<suggest>
52+
<content>Continue with more details</content>
53+
</suggest>
2554
</follow_up>
2655
</ask_followup_question>`
2756
}

src/core/tools/__tests__/askFollowupQuestionTool.spec.ts

Lines changed: 92 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,96 @@ describe("askFollowupQuestionTool", () => {
2121
})
2222
})
2323

24-
it("should parse suggestions without mode attributes", async () => {
24+
it("should parse suggestions without mode (new nested format)", async () => {
25+
const block: ToolUse = {
26+
type: "tool_use",
27+
name: "ask_followup_question",
28+
params: {
29+
question: "What would you like to do?",
30+
follow_up:
31+
"<suggest><content>Option 1</content></suggest><suggest><content>Option 2</content></suggest>",
32+
},
33+
partial: false,
34+
}
35+
36+
await askFollowupQuestionTool(
37+
mockCline,
38+
block,
39+
vi.fn(),
40+
vi.fn(),
41+
mockPushToolResult,
42+
vi.fn((tag, content) => content),
43+
)
44+
45+
expect(mockCline.ask).toHaveBeenCalledWith(
46+
"followup",
47+
expect.stringContaining('"suggest":[{"answer":"Option 1"},{"answer":"Option 2"}]'),
48+
false,
49+
)
50+
})
51+
52+
it("should parse suggestions with mode (new nested format)", async () => {
53+
const block: ToolUse = {
54+
type: "tool_use",
55+
name: "ask_followup_question",
56+
params: {
57+
question: "What would you like to do?",
58+
follow_up:
59+
"<suggest><mode>code</mode><content>Write code</content></suggest><suggest><mode>debug</mode><content>Debug issue</content></suggest>",
60+
},
61+
partial: false,
62+
}
63+
64+
await askFollowupQuestionTool(
65+
mockCline,
66+
block,
67+
vi.fn(),
68+
vi.fn(),
69+
mockPushToolResult,
70+
vi.fn((tag, content) => content),
71+
)
72+
73+
expect(mockCline.ask).toHaveBeenCalledWith(
74+
"followup",
75+
expect.stringContaining(
76+
'"suggest":[{"answer":"Write code","mode":"code"},{"answer":"Debug issue","mode":"debug"}]',
77+
),
78+
false,
79+
)
80+
})
81+
82+
it("should handle mixed suggestions with and without mode (new nested format)", async () => {
83+
const block: ToolUse = {
84+
type: "tool_use",
85+
name: "ask_followup_question",
86+
params: {
87+
question: "What would you like to do?",
88+
follow_up:
89+
"<suggest><content>Regular option</content></suggest><suggest><mode>architect</mode><content>Plan architecture</content></suggest>",
90+
},
91+
partial: false,
92+
}
93+
94+
await askFollowupQuestionTool(
95+
mockCline,
96+
block,
97+
vi.fn(),
98+
vi.fn(),
99+
mockPushToolResult,
100+
vi.fn((tag, content) => content),
101+
)
102+
103+
expect(mockCline.ask).toHaveBeenCalledWith(
104+
"followup",
105+
expect.stringContaining(
106+
'"suggest":[{"answer":"Regular option"},{"answer":"Plan architecture","mode":"architect"}]',
107+
),
108+
false,
109+
)
110+
})
111+
112+
// Backward compatibility tests for old format
113+
it("should parse suggestions without mode attributes (old format - backward compatibility)", async () => {
25114
const block: ToolUse = {
26115
type: "tool_use",
27116
name: "ask_followup_question",
@@ -48,7 +137,7 @@ describe("askFollowupQuestionTool", () => {
48137
)
49138
})
50139

51-
it("should parse suggestions with mode attributes", async () => {
140+
it("should parse suggestions with mode attributes (old format - backward compatibility)", async () => {
52141
const block: ToolUse = {
53142
type: "tool_use",
54143
name: "ask_followup_question",
@@ -77,7 +166,7 @@ describe("askFollowupQuestionTool", () => {
77166
)
78167
})
79168

80-
it("should handle mixed suggestions with and without mode attributes", async () => {
169+
it("should handle mixed suggestions with and without mode attributes (old format - backward compatibility)", async () => {
81170
const block: ToolUse = {
82171
type: "tool_use",
83172
name: "ask_followup_question",

src/core/tools/askFollowupQuestionTool.ts

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,18 @@ export async function askFollowupQuestionTool(
3434
}
3535

3636
if (follow_up) {
37-
// Define the actual structure returned by the XML parser
38-
type ParsedSuggestion = string | { "#text": string; "@_mode"?: string }
37+
// Define the actual structure returned by the XML parser for the new format
38+
type ParsedSuggestion =
39+
| string // For backward compatibility with old format or when stopNodes is used
40+
| { "#text": string; "@_mode"?: string } // For backward compatibility with old format
3941

4042
let parsedSuggest: {
4143
suggest: ParsedSuggestion[] | ParsedSuggestion
4244
}
4345

4446
try {
45-
parsedSuggest = parseXml(follow_up, ["suggest"]) as {
47+
// Don't use stopNodes for suggest elements to allow proper parsing of nested structure
48+
parsedSuggest = parseXml(follow_up) as {
4649
suggest: ParsedSuggestion[] | ParsedSuggestion
4750
}
4851
} catch (error) {
@@ -58,17 +61,34 @@ export async function askFollowupQuestionTool(
5861
: [parsedSuggest?.suggest].filter((sug): sug is ParsedSuggestion => sug !== undefined)
5962

6063
// Transform parsed XML to our Suggest format
61-
const normalizedSuggest: Suggest[] = rawSuggestions.map((sug) => {
64+
const normalizedSuggest: Suggest[] = rawSuggestions.map((sug: any) => {
6265
if (typeof sug === "string") {
63-
// Simple string suggestion (no mode attribute)
66+
// Simple string suggestion (backward compatibility)
6467
return { answer: sug }
65-
} else {
66-
// XML object with text content and optional mode attribute
67-
const result: Suggest = { answer: sug["#text"] }
68-
if (sug["@_mode"]) {
69-
result.mode = sug["@_mode"]
68+
} else if (sug && typeof sug === "object") {
69+
// Check for new nested element format
70+
if ("content" in sug) {
71+
const result: Suggest = { answer: sug.content }
72+
if (sug.mode) {
73+
result.mode = sug.mode
74+
}
75+
return result
76+
}
77+
// Old attribute format (backward compatibility)
78+
else if ("#text" in sug) {
79+
const result: Suggest = { answer: sug["#text"] }
80+
if (sug["@_mode"]) {
81+
result.mode = sug["@_mode"]
82+
}
83+
return result
7084
}
71-
return result
85+
// Fallback for any other object structure
86+
else {
87+
return { answer: JSON.stringify(sug) }
88+
}
89+
} else {
90+
// Fallback for any unexpected type
91+
return { answer: String(sug) }
7292
}
7393
})
7494

0 commit comments

Comments
 (0)