feat(core): default gpt-5.2-codex + request_user_input tool

This commit is contained in:
James Peter
2026-01-22 09:29:55 +10:00
parent 6265400805
commit 7aad17a511
7 changed files with 318 additions and 5 deletions
+7 -2
View File
@@ -617,6 +617,12 @@ impl ModelClient {
Some(request_id.clone()),
)
})?;
let ws_endpoint = match url.scheme() {
"http" => endpoint.replacen("http://", "ws://", 1),
"https" => endpoint.replacen("https://", "wss://", 1),
_ => endpoint.clone(),
};
let mut req_builder = self
.provider
.create_request_builder_for_url(&self.client, &auth, reqwest::Method::GET, url)
@@ -668,8 +674,7 @@ impl ModelClient {
.map(|req| req.headers().clone())
.unwrap_or_else(HeaderMap::new);
let mut ws_request = endpoint
.clone()
let mut ws_request = ws_endpoint
.into_client_request()
.map_err(|err| {
CodexErr::Stream(
+24
View File
@@ -132,6 +132,7 @@ pub(super) struct State {
pub(super) approved_commands: HashSet<ApprovedCommandPattern>,
pub(super) current_task: Option<AgentTask>,
pub(super) pending_approvals: HashMap<String, oneshot::Sender<ReviewDecision>>,
pub(super) pending_tool_user_input: VecDeque<oneshot::Sender<Vec<InputItem>>>,
pub(super) pending_input: Vec<ResponseInputItem>,
pub(super) pending_user_input: Vec<QueuedUserInput>,
pub(super) history: ConversationHistory,
@@ -1119,6 +1120,28 @@ impl Session {
}
}
pub async fn request_tool_user_input(&self) -> Option<Vec<InputItem>> {
let (tx, rx) = oneshot::channel();
{
let mut state = self.state.lock().unwrap();
state.pending_tool_user_input.push_back(tx);
}
rx.await.ok()
}
pub fn notify_tool_user_input(&self, items: Vec<InputItem>) -> Option<Vec<InputItem>> {
let pending = {
let mut state = self.state.lock().unwrap();
state.pending_tool_user_input.pop_front()
};
if let Some(tx) = pending {
let _ = tx.send(items);
None
} else {
Some(items)
}
}
pub fn add_approved_command(&self, pattern: ApprovedCommandPattern) {
let mut state = self.state.lock().unwrap();
state.approved_commands.insert(pattern);
@@ -1832,6 +1855,7 @@ impl Session {
let mut state = self.state.lock().unwrap();
state.pending_approvals.clear();
state.pending_tool_user_input.clear();
// Do not clear `pending_input` here. When a user submits a new message
// immediately after an interrupt, it may have been routed to
// `pending_input` by an earlier code path. Clearing it would drop the
+150
View File
@@ -804,6 +804,14 @@ pub(super) async fn submission_loop(
}
};
let items = match sess.notify_tool_user_input(items) {
Some(items) => items,
None => {
// Consumed as input for a pending `request_user_input` tool call.
continue;
}
};
// Clean up old status items when new user input arrives
// This prevents token buildup from old screenshots/status messages
sess.cleanup_old_status_items().await;
@@ -827,6 +835,14 @@ pub(super) async fn submission_loop(
}
};
let items = match sess.notify_tool_user_input(items) {
Some(items) => items,
None => {
// Consumed as input for a pending `request_user_input` tool call.
continue;
}
};
if sess.has_running_task() {
let mut response_item = response_input_from_core_items(items.clone());
sess.enforce_user_message_limits(&sub.id, &mut response_item);
@@ -2982,6 +2998,7 @@ async fn handle_function_call(
.await
}
"update_plan" => handle_update_plan(sess, &ctx, arguments).await,
"request_user_input" => handle_request_user_input(sess, &ctx, arguments).await,
// agent tool
"agent" => handle_agent_tool(sess, &ctx, arguments).await,
// unified browser tool
@@ -3013,6 +3030,139 @@ async fn handle_function_call(
}
}
async fn handle_request_user_input(
sess: &Session,
ctx: &ToolCallCtx,
arguments: String,
) -> ResponseInputItem {
use code_protocol::request_user_input::RequestUserInputAnswer;
use code_protocol::request_user_input::RequestUserInputArgs;
use code_protocol::request_user_input::RequestUserInputResponse;
let args: RequestUserInputArgs = match serde_json::from_str(&arguments) {
Ok(args) => args,
Err(err) => {
return ResponseInputItem::FunctionCallOutput {
call_id: ctx.call_id.clone(),
output: FunctionCallOutputPayload {
content: format!("invalid request_user_input arguments: {err}"),
success: Some(false),
},
};
}
};
if args.questions.is_empty() {
return ResponseInputItem::FunctionCallOutput {
call_id: ctx.call_id.clone(),
output: FunctionCallOutputPayload {
content: "request_user_input requires at least one question".to_string(),
success: Some(false),
},
};
}
let mut prompt = String::from("The model requested user input:\n");
for (idx, q) in args.questions.iter().enumerate() {
prompt.push_str(&format!(
"\n{}. {} ({})\n{}\n",
idx + 1,
q.header,
q.id,
q.question
));
if let Some(options) = &q.options {
prompt.push_str("Options:\n");
for option in options {
prompt.push_str(&format!("- {}: {}\n", option.label, option.description));
}
}
}
if args.questions.len() == 1 {
prompt.push_str("\nReply with your answer.");
} else {
prompt.push_str("\nReply with one line per question (in order). Extra lines are attached to the final question.");
}
sess.send_ordered_from_ctx(
ctx,
EventMsg::AgentMessage(AgentMessageEvent {
message: prompt,
}),
)
.await;
let user_items = sess.request_tool_user_input().await;
let Some(user_items) = user_items else {
return ResponseInputItem::FunctionCallOutput {
call_id: ctx.call_id.clone(),
output: FunctionCallOutputPayload {
content: "request_user_input was cancelled before receiving a response".to_string(),
success: Some(false),
},
};
};
let user_text = user_items
.iter()
.filter_map(|item| match item {
InputItem::Text { text } => Some(text.trim().to_string()),
_ => None,
})
.collect::<Vec<_>>()
.join("\n")
.trim()
.to_string();
let response: RequestUserInputResponse = serde_json::from_str(&user_text).unwrap_or_else(|_| {
let mut answers = std::collections::HashMap::new();
let mut lines: Vec<String> = user_text
.lines()
.map(|line| line.trim_end().to_string())
.collect();
if args.questions.len() == 1 {
lines = vec![user_text.clone()];
} else if lines.len() > args.questions.len() {
let tail = lines.split_off(args.questions.len() - 1);
lines.push(tail.join("\n"));
}
for (idx, q) in args.questions.iter().enumerate() {
let value = lines.get(idx).cloned().unwrap_or_default();
answers.insert(
q.id.clone(),
RequestUserInputAnswer {
answers: vec![value],
},
);
}
RequestUserInputResponse { answers }
});
let content = match serde_json::to_string(&response) {
Ok(content) => content,
Err(err) => {
return ResponseInputItem::FunctionCallOutput {
call_id: ctx.call_id.clone(),
output: FunctionCallOutputPayload {
content: format!("failed to serialize request_user_input response: {err}"),
success: Some(false),
},
};
}
};
ResponseInputItem::FunctionCallOutput {
call_id: ctx.call_id.clone(),
output: FunctionCallOutputPayload {
content,
success: Some(true),
},
}
}
async fn handle_browser_cleanup(sess: &Session, ctx: &ToolCallCtx) -> ResponseInputItem {
let call_id_clone = ctx.call_id.clone();
let _sess_clone = sess;
+3 -3
View File
@@ -101,8 +101,8 @@ pub use crate::config_constraint::ConstraintResult;
pub(crate) use defaults::merge_with_default_agents;
pub(crate) use validation::upgrade_legacy_model_slugs;
pub(crate) const OPENAI_DEFAULT_MODEL: &str = "gpt-5.1-codex-max";
const OPENAI_DEFAULT_REVIEW_MODEL: &str = "gpt-5.1-codex-max";
pub(crate) const OPENAI_DEFAULT_MODEL: &str = "gpt-5.2-codex";
const OPENAI_DEFAULT_REVIEW_MODEL: &str = "gpt-5.2-codex";
pub const GPT_5_CODEX_MEDIUM_MODEL: &str = "gpt-5.2-codex";
/// Maximum number of bytes of the documentation that will be embedded. Larger
@@ -133,7 +133,7 @@ pub struct Config {
/// Whether planning should inherit the chat model instead of using a dedicated override.
pub planning_use_chat_model: bool,
/// Model used specifically for review sessions. Defaults to "gpt-5.1-codex-max".
/// Model used specifically for review sessions. Defaults to "gpt-5.2-codex".
pub review_model: String,
/// Reasoning effort used when running review sessions.
+85
View File
@@ -393,6 +393,89 @@ fn create_image_view_tool() -> OpenAiTool {
})
}
fn create_request_user_input_tool() -> OpenAiTool {
let mut option_props = BTreeMap::new();
option_props.insert(
"label".to_string(),
JsonSchema::String {
description: Some("User-facing label (1-5 words).".to_string()),
allowed_values: None,
},
);
option_props.insert(
"description".to_string(),
JsonSchema::String {
description: Some(
"One short sentence explaining impact/tradeoff if selected.".to_string(),
),
allowed_values: None,
},
);
let options_schema = JsonSchema::Array {
description: Some(
"Optional 2-3 mutually exclusive choices. Put the recommended option first and suffix its label with \"(Recommended)\". Only include \"Other\" option if we want to include a free form option. If the question is free form in nature, please do not have any option.".to_string(),
),
items: Box::new(JsonSchema::Object {
properties: option_props,
required: Some(vec!["label".to_string(), "description".to_string()]),
additional_properties: Some(false.into()),
}),
};
let mut question_props = BTreeMap::new();
question_props.insert(
"id".to_string(),
JsonSchema::String {
description: Some("Stable identifier for mapping answers (snake_case).".to_string()),
allowed_values: None,
},
);
question_props.insert(
"header".to_string(),
JsonSchema::String {
description: Some("Short header label shown in the UI (12 or fewer chars).".to_string()),
allowed_values: None,
},
);
question_props.insert(
"question".to_string(),
JsonSchema::String {
description: Some("Single-sentence prompt shown to the user.".to_string()),
allowed_values: None,
},
);
question_props.insert("options".to_string(), options_schema);
let questions_schema = JsonSchema::Array {
description: Some("Questions to show the user. Prefer 1 and do not exceed 3".to_string()),
items: Box::new(JsonSchema::Object {
properties: question_props,
required: Some(vec![
"id".to_string(),
"header".to_string(),
"question".to_string(),
]),
additional_properties: Some(false.into()),
}),
};
let mut properties = BTreeMap::new();
properties.insert("questions".to_string(), questions_schema);
OpenAiTool::Function(ResponsesApiTool {
name: "request_user_input".to_string(),
description: "Request user input for one to three short questions and wait for the response."
.to_string(),
strict: false,
parameters: JsonSchema::Object {
properties,
required: Some(vec!["questions".to_string()]),
additional_properties: Some(false.into()),
},
})
}
fn create_shell_tool_for_sandbox(sandbox_policy: &SandboxPolicy) -> OpenAiTool {
let mut properties = BTreeMap::new();
properties.insert(
@@ -741,6 +824,8 @@ pub fn get_openai_tools(
tools.push(PLAN_TOOL.clone());
}
tools.push(create_request_user_input_tool());
tools.push(create_browser_tool(browser_enabled));
// Add agent management tool for launching and monitoring asynchronous agents
+1
View File
@@ -9,4 +9,5 @@ pub mod num_format;
pub mod parse_command;
pub mod plan_tool;
pub mod protocol;
pub mod request_user_input;
pub use mcp_protocol::ConversationId;
@@ -0,0 +1,48 @@
use std::collections::HashMap;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct RequestUserInputQuestionOption {
pub label: String,
pub description: String,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct RequestUserInputQuestion {
pub id: String,
pub header: String,
pub question: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<Vec<RequestUserInputQuestionOption>>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct RequestUserInputArgs {
pub questions: Vec<RequestUserInputQuestion>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct RequestUserInputAnswer {
pub answers: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct RequestUserInputResponse {
pub answers: HashMap<String, RequestUserInputAnswer>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
pub struct RequestUserInputEvent {
/// Responses API call id for the associated tool call, if available.
pub call_id: String,
/// Turn ID that this request belongs to.
/// Uses `#[serde(default)]` for backwards compatibility.
#[serde(default)]
pub turn_id: String,
pub questions: Vec<RequestUserInputQuestion>,
}