Chore: update plan mode output in prompt (#9592)

### Summary
* Update plan prompt output
* Update requestUserInput response to be a single key value pair
`answer: String`.
This commit is contained in:
Shijie Rao
2026-01-21 14:12:18 -08:00
committed by GitHub
Unverified
parent f2e1ad59bc
commit 3fcb40245e
7 changed files with 30 additions and 44 deletions
@@ -2327,8 +2327,7 @@ pub struct ToolRequestUserInputParams {
#[ts(export_to = "v2/")]
/// EXPERIMENTAL. Captures a user's answer to a request_user_input question.
pub struct ToolRequestUserInputAnswer {
pub selected: Vec<String>,
pub other: Option<String>,
pub answers: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
@@ -1445,8 +1445,7 @@ async fn on_request_user_input_response(
(
id,
CoreRequestUserInputAnswer {
selected: answer.selected,
other: answer.other,
answers: answer.answers,
},
)
})
@@ -87,7 +87,7 @@ async fn request_user_input_round_trip() -> Result<()> {
request_id,
serde_json::json!({
"answers": {
"confirm_path": { "selected": ["yes"], "other": serde_json::Value::Null }
"confirm_path": { "answers": ["yes"] }
}
}),
)
@@ -33,7 +33,7 @@ Example: "I checked the readme and searched for the feature you mentioned, but d
Use `request_user_input` only when you are genuinely blocked on a decision that materially changes the plan (requirements, trade-offs, rollout or risk posture).The maximum number of `request_user_input` tool calls should be **5**.
Only include an "Other" option when a free-form answer is truly useful. If the question is purely free-form, leave `options` unset entirely.
**The options should be mutually exclusive.** Only include an "Other" option when a free-form answer is truly useful. If the question is purely free-form, leave `options` unset entirely.
Do **not** use `request_user_input` to ask "is my plan ready?" or "should I proceed?".
@@ -112,22 +112,12 @@ A well written and informative plan should be as detailed as a design doc or PRD
- tools and frameworks you use, any dependencies you need to install
- functions, files, or directories you're likely going to edit
- QUestions that were asked and the responses from users
- Questions that were asked and the responses from users
- architecture if the code changes are significant
- if developing features, describe the features you are going to build in detail like a PM in a PRD
- if you are developing a frontend, describe the design in detail
- include a list of todos in markdown format if needed. Please do not include a **plan** step given that we are planning here already
### Output schema - — MUST MATCH _exactly_
### Plan output
When you present the plan, format the final response as a JSON object with a single key, `plan`, whose value is the full plan text.
Example:
```json
{
"plan": "Title: Schema migration rollout\n\n1. Validate the current schema on staging...\n2. Add the new columns with nullable defaults...\n3. Backfill in batches with feature-flagged writes...\n4. Flip reads to the new fields and monitor...\n5. Remove legacy columns after one full release cycle..."
}
```
PLEASE DO NOT confirm the plan with the user before ending. The user will be responsible for telling us to update, iterate or execute the plan.
**The final output should contain the plan and plan only with a good title.** PLEASE DO NOT confirm the plan with the user before ending. The user will be responsible for telling us to update, iterate or execute the plan. The
@@ -152,8 +152,7 @@ async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()>
answers.insert(
"confirm_path".to_string(),
RequestUserInputAnswer {
selected: vec!["yes".to_string()],
other: None,
answers: vec!["yes".to_string()],
},
);
let response = RequestUserInputResponse { answers };
@@ -173,7 +172,7 @@ async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()>
output_json,
json!({
"answers": {
"confirm_path": { "selected": ["yes"], "other": Value::Null }
"confirm_path": { "answers": ["yes"] }
}
})
);
+1 -2
View File
@@ -27,8 +27,7 @@ pub struct RequestUserInputArgs {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct RequestUserInputAnswer {
pub selected: Vec<String>,
pub other: Option<String>,
pub answers: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
@@ -2,10 +2,10 @@
//!
//! Core behaviors:
//! - Each question can be answered by selecting one option and/or providing notes.
//! - When options exist, notes are stored per selected option (notes become "other").
//! - When options exist, notes are stored per selected option and appended as extra answers.
//! - Typing while focused on options jumps into notes to keep freeform input fast.
//! - Enter advances to the next question; the last question submits all answers.
//! - Freeform-only questions submit "skipped" when empty.
//! - Freeform-only questions submit an empty answer list when empty.
use std::cell::RefCell;
use std::collections::HashMap;
use std::collections::VecDeque;
@@ -278,7 +278,7 @@ impl RequestUserInputOverlay {
} else {
answer_state.selected
};
// Notes map to "other". When options exist, notes are per selected option.
// Notes are appended as extra answers. When options exist, notes are per selected option.
let notes = if options.is_some_and(|opts| !opts.is_empty()) {
selected_idx
.and_then(|selected| answer_state.option_notes.get(selected))
@@ -294,18 +294,15 @@ impl RequestUserInputOverlay {
.and_then(|opts| opts.get(selected_idx))
.map(|opt| opt.label.clone())
});
let selected = selected_label.into_iter().collect::<Vec<_>>();
// For option questions, only send notes when present.
let other = if notes.is_empty() && options.is_some_and(|opts| !opts.is_empty()) {
None
} else if notes.is_empty() && selected.is_empty() {
Some("skipped".to_string())
} else {
Some(notes)
};
let mut answer_list = selected_label.into_iter().collect::<Vec<_>>();
if !notes.is_empty() {
answer_list.push(format!("user_note: {notes}"));
}
answers.insert(
question.id.clone(),
RequestUserInputAnswer { selected, other },
RequestUserInputAnswer {
answers: answer_list,
},
);
}
self.app_event_tx
@@ -605,12 +602,11 @@ mod tests {
};
assert_eq!(id, "turn-1");
let answer = response.answers.get("q1").expect("answer missing");
assert_eq!(answer.selected, vec!["Option 1".to_string()]);
assert_eq!(answer.other, None);
assert_eq!(answer.answers, vec!["Option 1".to_string()]);
}
#[test]
fn freeform_questions_submit_skipped_when_empty() {
fn freeform_questions_submit_empty_when_empty() {
let (tx, mut rx) = test_sender();
let mut overlay = RequestUserInputOverlay::new(
request_event("turn-1", vec![question_without_options("q1", "Notes")]),
@@ -624,8 +620,7 @@ mod tests {
panic!("expected UserInputAnswer");
};
let answer = response.answers.get("q1").expect("answer missing");
assert_eq!(answer.selected, Vec::<String>::new());
assert_eq!(answer.other, Some("skipped".to_string()));
assert_eq!(answer.answers, Vec::<String>::new());
}
#[test]
@@ -654,8 +649,13 @@ mod tests {
panic!("expected UserInputAnswer");
};
let answer = response.answers.get("q1").expect("answer missing");
assert_eq!(answer.selected, vec!["Option 2".to_string()]);
assert_eq!(answer.other, Some("Notes for option 2".to_string()));
assert_eq!(
answer.answers,
vec![
"Option 2".to_string(),
"user_note: Notes for option 2".to_string(),
]
);
}
#[test]