mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(core): default gpt-5.2-codex + request_user_input tool
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user