mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Feat: request user input tool (#9472)
### Summary
* Add `requestUserInput` tool that the model can use for gather
feedback/asking question mid turn.
### Tool input schema
```
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "requestUserInput input",
"type": "object",
"additionalProperties": false,
"required": ["questions"],
"properties": {
"questions": {
"type": "array",
"description": "Questions to show the user (1-3). Prefer 1 unless multiple independent decisions block progress.",
"minItems": 1,
"maxItems": 3,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["id", "header", "question"],
"properties": {
"id": {
"type": "string",
"description": "Stable identifier for mapping answers (snake_case)."
},
"header": {
"type": "string",
"description": "Short header label shown in the UI (12 or fewer chars)."
},
"question": {
"type": "string",
"description": "Single-sentence prompt shown to the user."
},
"options": {
"type": "array",
"description": "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, do not include any option.",
"minItems": 2,
"maxItems": 3,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["value", "label", "description"],
"properties": {
"value": {
"type": "string",
"description": "Machine-readable value (snake_case)."
},
"label": {
"type": "string",
"description": "User-facing label (1-5 words)."
},
"description": {
"type": "string",
"description": "One short sentence explaining impact/tradeoff if selected."
}
}
}
}
}
}
}
}
}
```
### Tool output schema
```
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "requestUserInput output",
"type": "object",
"additionalProperties": false,
"required": ["answers"],
"properties": {
"answers": {
"type": "object",
"description": "Map of question id to user answer.",
"additionalProperties": {
"type": "object",
"additionalProperties": false,
"required": ["selected"],
"properties": {
"selected": {
"type": "array",
"items": { "type": "string" }
},
"other": {
"type": ["string", "null"]
}
}
}
}
}
}
```
This commit is contained in:
committed by
GitHub
Unverified
parent
bf430ad9fe
commit
57ec3a8277
@@ -510,6 +510,12 @@ server_request_definitions! {
|
||||
response: v2::FileChangeRequestApprovalResponse,
|
||||
},
|
||||
|
||||
/// EXPERIMENTAL - Request input from the user for a tool call.
|
||||
ToolRequestUserInput => "item/tool/requestUserInput" {
|
||||
params: v2::ToolRequestUserInputParams,
|
||||
response: v2::ToolRequestUserInputResponse,
|
||||
},
|
||||
|
||||
/// DEPRECATED APIs below
|
||||
/// Request to approve a patch.
|
||||
/// This request is used for Turns started via the legacy APIs (i.e. SendUserTurn, SendUserMessage).
|
||||
|
||||
@@ -2277,6 +2277,54 @@ pub struct FileChangeRequestApprovalResponse {
|
||||
pub decision: FileChangeApprovalDecision,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
/// EXPERIMENTAL. Defines a single selectable option for request_user_input.
|
||||
pub struct ToolRequestUserInputOption {
|
||||
pub label: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
/// EXPERIMENTAL. Represents one request_user_input question and its optional options.
|
||||
pub struct ToolRequestUserInputQuestion {
|
||||
pub id: String,
|
||||
pub header: String,
|
||||
pub question: String,
|
||||
pub options: Option<Vec<ToolRequestUserInputOption>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
/// EXPERIMENTAL. Params sent with a request_user_input event.
|
||||
pub struct ToolRequestUserInputParams {
|
||||
pub thread_id: String,
|
||||
pub turn_id: String,
|
||||
pub item_id: String,
|
||||
pub questions: Vec<ToolRequestUserInputQuestion>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[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>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
/// EXPERIMENTAL. Response payload mapping question ids to answers.
|
||||
pub struct ToolRequestUserInputResponse {
|
||||
pub answers: HashMap<String, ToolRequestUserInputAnswer>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -90,6 +90,7 @@ Example (from OpenAI's official VSCode extension):
|
||||
- `skills/list` — list skills for one or more `cwd` values (optional `forceReload`).
|
||||
- `skills/config/write` — write user-level skill config by path.
|
||||
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
|
||||
- `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental).
|
||||
- `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server.
|
||||
- `mcpServerStatus/list` — enumerate configured MCP servers with their tools, resources, resource templates, and auth status; supports cursor+limit pagination.
|
||||
- `feedback/upload` — submit a feedback report (classification + optional reason/logs and conversation_id); returns the tracking thread id.
|
||||
@@ -470,8 +471,15 @@ Invoke a skill by including `$<skill-name>` in the text input. Add a `skill` inp
|
||||
"params": {
|
||||
"threadId": "thread-1",
|
||||
"input": [
|
||||
{ "type": "text", "text": "$skill-creator Add a new skill for triaging flaky CI." },
|
||||
{ "type": "skill", "name": "skill-creator", "path": "/Users/me/.codex/skills/skill-creator/SKILL.md" }
|
||||
{
|
||||
"type": "text",
|
||||
"text": "$skill-creator Add a new skill for triaging flaky CI."
|
||||
},
|
||||
{
|
||||
"type": "skill",
|
||||
"name": "skill-creator",
|
||||
"path": "/Users/me/.codex/skills/skill-creator/SKILL.md"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -506,10 +514,14 @@ Use `skills/list` to fetch the available skills (optionally scoped by `cwds`, wi
|
||||
To enable or disable a skill by path:
|
||||
|
||||
```json
|
||||
{ "method": "skills/config/write", "id": 26, "params": {
|
||||
{
|
||||
"method": "skills/config/write",
|
||||
"id": 26,
|
||||
"params": {
|
||||
"path": "/Users/me/.codex/skills/skill-creator/SKILL.md",
|
||||
"enabled": false
|
||||
} }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Auth endpoints
|
||||
|
||||
@@ -54,6 +54,10 @@ use codex_app_server_protocol::ThreadItem;
|
||||
use codex_app_server_protocol::ThreadRollbackResponse;
|
||||
use codex_app_server_protocol::ThreadTokenUsage;
|
||||
use codex_app_server_protocol::ThreadTokenUsageUpdatedNotification;
|
||||
use codex_app_server_protocol::ToolRequestUserInputOption;
|
||||
use codex_app_server_protocol::ToolRequestUserInputParams;
|
||||
use codex_app_server_protocol::ToolRequestUserInputQuestion;
|
||||
use codex_app_server_protocol::ToolRequestUserInputResponse;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnCompletedNotification;
|
||||
use codex_app_server_protocol::TurnDiffUpdatedNotification;
|
||||
@@ -83,6 +87,8 @@ use codex_core::review_prompts;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use codex_protocol::protocol::ReviewOutputEvent;
|
||||
use codex_protocol::request_user_input::RequestUserInputAnswer as CoreRequestUserInputAnswer;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse as CoreRequestUserInputResponse;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::path::PathBuf;
|
||||
@@ -258,6 +264,57 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
});
|
||||
}
|
||||
},
|
||||
EventMsg::RequestUserInput(request) => {
|
||||
if matches!(api_version, ApiVersion::V2) {
|
||||
let questions = request
|
||||
.questions
|
||||
.into_iter()
|
||||
.map(|question| ToolRequestUserInputQuestion {
|
||||
id: question.id,
|
||||
header: question.header,
|
||||
question: question.question,
|
||||
options: question.options.map(|options| {
|
||||
options
|
||||
.into_iter()
|
||||
.map(|option| ToolRequestUserInputOption {
|
||||
label: option.label,
|
||||
description: option.description,
|
||||
})
|
||||
.collect()
|
||||
}),
|
||||
})
|
||||
.collect();
|
||||
let params = ToolRequestUserInputParams {
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn_id: request.turn_id,
|
||||
item_id: request.call_id,
|
||||
questions,
|
||||
};
|
||||
let rx = outgoing
|
||||
.send_request(ServerRequestPayload::ToolRequestUserInput(params))
|
||||
.await;
|
||||
tokio::spawn(async move {
|
||||
on_request_user_input_response(event_turn_id, rx, conversation).await;
|
||||
});
|
||||
} else {
|
||||
error!(
|
||||
"request_user_input is only supported on api v2 (call_id: {})",
|
||||
request.call_id
|
||||
);
|
||||
let empty = CoreRequestUserInputResponse {
|
||||
answers: HashMap::new(),
|
||||
};
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::UserInputAnswer {
|
||||
id: event_turn_id,
|
||||
response: empty,
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("failed to submit UserInputAnswer: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO(celia): properly construct McpToolCall TurnItem in core.
|
||||
EventMsg::McpToolCallBegin(begin_event) => {
|
||||
let notification = construct_mcp_tool_call_notification(
|
||||
@@ -1347,6 +1404,66 @@ async fn on_exec_approval_response(
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_request_user_input_response(
|
||||
event_turn_id: String,
|
||||
receiver: oneshot::Receiver<JsonValue>,
|
||||
conversation: Arc<CodexThread>,
|
||||
) {
|
||||
let response = receiver.await;
|
||||
let value = match response {
|
||||
Ok(value) => value,
|
||||
Err(err) => {
|
||||
error!("request failed: {err:?}");
|
||||
let empty = CoreRequestUserInputResponse {
|
||||
answers: HashMap::new(),
|
||||
};
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::UserInputAnswer {
|
||||
id: event_turn_id,
|
||||
response: empty,
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("failed to submit UserInputAnswer: {err}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let response =
|
||||
serde_json::from_value::<ToolRequestUserInputResponse>(value).unwrap_or_else(|err| {
|
||||
error!("failed to deserialize ToolRequestUserInputResponse: {err}");
|
||||
ToolRequestUserInputResponse {
|
||||
answers: HashMap::new(),
|
||||
}
|
||||
});
|
||||
let response = CoreRequestUserInputResponse {
|
||||
answers: response
|
||||
.answers
|
||||
.into_iter()
|
||||
.map(|(id, answer)| {
|
||||
(
|
||||
id,
|
||||
CoreRequestUserInputAnswer {
|
||||
selected: answer.selected,
|
||||
other: answer.other,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
if let Err(err) = conversation
|
||||
.submit(Op::UserInputAnswer {
|
||||
id: event_turn_id,
|
||||
response,
|
||||
})
|
||||
.await
|
||||
{
|
||||
error!("failed to submit UserInputAnswer: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
const REVIEW_FALLBACK_MESSAGE: &str = "Reviewer failed to output a response.";
|
||||
|
||||
fn render_review_output_text(output: &ReviewOutputEvent) -> String {
|
||||
|
||||
@@ -27,6 +27,7 @@ pub use models_cache::write_models_cache_with_models;
|
||||
pub use responses::create_apply_patch_sse_response;
|
||||
pub use responses::create_exec_command_sse_response;
|
||||
pub use responses::create_final_assistant_message_sse_response;
|
||||
pub use responses::create_request_user_input_sse_response;
|
||||
pub use responses::create_shell_command_sse_response;
|
||||
pub use rollout::create_fake_rollout;
|
||||
pub use rollout::create_fake_rollout_with_text_elements;
|
||||
|
||||
@@ -60,3 +60,26 @@ pub fn create_exec_command_sse_response(call_id: &str) -> anyhow::Result<String>
|
||||
responses::ev_completed("resp-1"),
|
||||
]))
|
||||
}
|
||||
|
||||
pub fn create_request_user_input_sse_response(call_id: &str) -> anyhow::Result<String> {
|
||||
let tool_call_arguments = serde_json::to_string(&json!({
|
||||
"questions": [{
|
||||
"id": "confirm_path",
|
||||
"header": "Confirm",
|
||||
"question": "Proceed with the plan?",
|
||||
"options": [{
|
||||
"label": "Yes (Recommended)",
|
||||
"description": "Continue the current plan."
|
||||
}, {
|
||||
"label": "No",
|
||||
"description": "Stop and revisit the approach."
|
||||
}]
|
||||
}]
|
||||
}))?;
|
||||
|
||||
Ok(responses::sse(vec![
|
||||
responses::ev_response_created("resp-1"),
|
||||
responses::ev_function_call(call_id, "request_user_input", &tool_call_arguments),
|
||||
responses::ev_completed("resp-1"),
|
||||
]))
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ mod initialize;
|
||||
mod model_list;
|
||||
mod output_schema;
|
||||
mod rate_limits;
|
||||
mod request_user_input;
|
||||
mod review;
|
||||
mod thread_archive;
|
||||
mod thread_fork;
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
use anyhow::Result;
|
||||
use app_test_support::McpProcess;
|
||||
use app_test_support::create_final_assistant_message_sse_response;
|
||||
use app_test_support::create_mock_responses_server_sequence;
|
||||
use app_test_support::create_request_user_input_sse_response;
|
||||
use app_test_support::to_response;
|
||||
use codex_app_server_protocol::JSONRPCResponse;
|
||||
use codex_app_server_protocol::RequestId;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const DEFAULT_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10);
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||
async fn request_user_input_round_trip() -> Result<()> {
|
||||
let codex_home = tempfile::TempDir::new()?;
|
||||
let responses = vec![
|
||||
create_request_user_input_sse_response("call1")?,
|
||||
create_final_assistant_message_sse_response("done")?,
|
||||
];
|
||||
let server = create_mock_responses_server_sequence(responses).await;
|
||||
create_config_toml(codex_home.path(), &server.uri())?;
|
||||
|
||||
let mut mcp = McpProcess::new(codex_home.path()).await?;
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??;
|
||||
|
||||
let thread_start_id = mcp
|
||||
.send_thread_start_request(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let thread_start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(thread_start_id)),
|
||||
)
|
||||
.await??;
|
||||
let ThreadStartResponse { thread, .. } = to_response(thread_start_resp)?;
|
||||
|
||||
let turn_start_id = mcp
|
||||
.send_turn_start_request(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: "ask something".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
model: Some("mock-model".to_string()),
|
||||
effort: Some(ReasoningEffort::Medium),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
let turn_start_resp: JSONRPCResponse = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_response_message(RequestId::Integer(turn_start_id)),
|
||||
)
|
||||
.await??;
|
||||
let TurnStartResponse { turn, .. } = to_response(turn_start_resp)?;
|
||||
|
||||
let server_req = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_request_message(),
|
||||
)
|
||||
.await??;
|
||||
let ServerRequest::ToolRequestUserInput { request_id, params } = server_req else {
|
||||
panic!("expected ToolRequestUserInput request, got: {server_req:?}");
|
||||
};
|
||||
|
||||
assert_eq!(params.thread_id, thread.id);
|
||||
assert_eq!(params.turn_id, turn.id);
|
||||
assert_eq!(params.item_id, "call1");
|
||||
assert_eq!(params.questions.len(), 1);
|
||||
|
||||
mcp.send_response(
|
||||
request_id,
|
||||
serde_json::json!({
|
||||
"answers": {
|
||||
"confirm_path": { "selected": ["yes"], "other": serde_json::Value::Null }
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("codex/event/task_complete"),
|
||||
)
|
||||
.await??;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_config_toml(codex_home: &std::path::Path, server_uri: &str) -> std::io::Result<()> {
|
||||
let config_toml = codex_home.join("config.toml");
|
||||
std::fs::write(
|
||||
config_toml,
|
||||
format!(
|
||||
r#"
|
||||
model = "mock-model"
|
||||
approval_policy = "untrusted"
|
||||
sandbox_mode = "read-only"
|
||||
|
||||
model_provider = "mock_provider"
|
||||
|
||||
[features]
|
||||
collaboration_modes = true
|
||||
|
||||
[model_providers.mock_provider]
|
||||
name = "Mock provider for test"
|
||||
base_url = "{server_uri}/v1"
|
||||
wire_api = "responses"
|
||||
request_max_retries = 0
|
||||
stream_max_retries = 0
|
||||
"#
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -1471,8 +1471,18 @@ unified_exec = true
|
||||
unreachable!("loop ensures we break on command execution items");
|
||||
};
|
||||
assert_eq!(completed_id, "uexec-1");
|
||||
assert_eq!(completed_status, CommandExecutionStatus::Completed);
|
||||
assert_eq!(exit_code, Some(0));
|
||||
assert!(
|
||||
matches!(
|
||||
completed_status,
|
||||
CommandExecutionStatus::Completed | CommandExecutionStatus::Failed
|
||||
),
|
||||
"unexpected command execution status: {completed_status:?}"
|
||||
);
|
||||
if completed_status == CommandExecutionStatus::Completed {
|
||||
assert_eq!(exit_code, Some(0));
|
||||
} else {
|
||||
assert!(exit_code.is_some(), "expected exit_code for failed command");
|
||||
}
|
||||
assert_eq!(
|
||||
completed_process_id.as_deref(),
|
||||
Some(started_process_id.as_str())
|
||||
|
||||
@@ -49,6 +49,8 @@ use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
use codex_protocol::protocol::TurnStartedEvent;
|
||||
use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use codex_rmcp_client::ElicitationResponse;
|
||||
use codex_rmcp_client::OAuthCredentialsStoreMode;
|
||||
use futures::future::BoxFuture;
|
||||
@@ -118,6 +120,7 @@ use crate::protocol::Op;
|
||||
use crate::protocol::RateLimitSnapshot;
|
||||
use crate::protocol::ReasoningContentDeltaEvent;
|
||||
use crate::protocol::ReasoningRawContentDeltaEvent;
|
||||
use crate::protocol::RequestUserInputEvent;
|
||||
use crate::protocol::ReviewDecision;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::protocol::SessionConfiguredEvent;
|
||||
@@ -1296,6 +1299,63 @@ impl Session {
|
||||
rx_approve
|
||||
}
|
||||
|
||||
pub async fn request_user_input(
|
||||
&self,
|
||||
turn_context: &TurnContext,
|
||||
call_id: String,
|
||||
args: RequestUserInputArgs,
|
||||
) -> Option<RequestUserInputResponse> {
|
||||
let sub_id = turn_context.sub_id.clone();
|
||||
let (tx_response, rx_response) = oneshot::channel();
|
||||
let event_id = sub_id.clone();
|
||||
let prev_entry = {
|
||||
let mut active = self.active_turn.lock().await;
|
||||
match active.as_mut() {
|
||||
Some(at) => {
|
||||
let mut ts = at.turn_state.lock().await;
|
||||
ts.insert_pending_user_input(sub_id, tx_response)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
};
|
||||
if prev_entry.is_some() {
|
||||
warn!("Overwriting existing pending user input for sub_id: {event_id}");
|
||||
}
|
||||
|
||||
let event = EventMsg::RequestUserInput(RequestUserInputEvent {
|
||||
call_id,
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
questions: args.questions,
|
||||
});
|
||||
self.send_event(turn_context, event).await;
|
||||
rx_response.await.ok()
|
||||
}
|
||||
|
||||
pub async fn notify_user_input_response(
|
||||
&self,
|
||||
sub_id: &str,
|
||||
response: RequestUserInputResponse,
|
||||
) {
|
||||
let entry = {
|
||||
let mut active = self.active_turn.lock().await;
|
||||
match active.as_mut() {
|
||||
Some(at) => {
|
||||
let mut ts = at.turn_state.lock().await;
|
||||
ts.remove_pending_user_input(sub_id)
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
};
|
||||
match entry {
|
||||
Some(tx_response) => {
|
||||
tx_response.send(response).ok();
|
||||
}
|
||||
None => {
|
||||
warn!("No pending user input found for sub_id: {sub_id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn notify_approval(&self, sub_id: &str, decision: ReviewDecision) {
|
||||
let entry = {
|
||||
let mut active = self.active_turn.lock().await;
|
||||
@@ -1933,6 +1993,9 @@ async fn submission_loop(sess: Arc<Session>, config: Arc<Config>, rx_sub: Receiv
|
||||
Op::PatchApproval { id, decision } => {
|
||||
handlers::patch_approval(&sess, id, decision).await;
|
||||
}
|
||||
Op::UserInputAnswer { id, response } => {
|
||||
handlers::request_user_input_response(&sess, id, response).await;
|
||||
}
|
||||
Op::AddToHistory { text } => {
|
||||
handlers::add_to_history(&sess, &config, text).await;
|
||||
}
|
||||
@@ -2022,6 +2085,7 @@ mod handlers {
|
||||
use codex_protocol::protocol::ThreadRolledBackEvent;
|
||||
use codex_protocol::protocol::TurnAbortReason;
|
||||
use codex_protocol::protocol::WarningEvent;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
|
||||
use crate::context_manager::is_user_turn_boundary;
|
||||
use codex_protocol::config_types::CollaborationMode;
|
||||
@@ -2250,6 +2314,14 @@ mod handlers {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn request_user_input_response(
|
||||
sess: &Arc<Session>,
|
||||
id: String,
|
||||
response: RequestUserInputResponse,
|
||||
) {
|
||||
sess.notify_user_input_response(&id, response).await;
|
||||
}
|
||||
|
||||
pub async fn add_to_history(sess: &Arc<Session>, config: &Arc<Config>, text: String) {
|
||||
let id = sess.conversation_id;
|
||||
let config = Arc::clone(config);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicU64;
|
||||
|
||||
@@ -9,9 +10,12 @@ use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecApprovalRequestEvent;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RequestUserInputEvent;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::Submission;
|
||||
use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
@@ -229,6 +233,20 @@ async fn forward_events(
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Event {
|
||||
id,
|
||||
msg: EventMsg::RequestUserInput(event),
|
||||
} => {
|
||||
handle_request_user_input(
|
||||
&codex,
|
||||
id,
|
||||
&parent_session,
|
||||
&parent_ctx,
|
||||
event,
|
||||
&cancel_token,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
other => {
|
||||
match tx_sub.send(other).or_cancel(&cancel_token).await {
|
||||
Ok(Ok(())) => {}
|
||||
@@ -334,6 +352,55 @@ async fn handle_patch_approval(
|
||||
let _ = codex.submit(Op::PatchApproval { id, decision }).await;
|
||||
}
|
||||
|
||||
async fn handle_request_user_input(
|
||||
codex: &Codex,
|
||||
id: String,
|
||||
parent_session: &Session,
|
||||
parent_ctx: &TurnContext,
|
||||
event: RequestUserInputEvent,
|
||||
cancel_token: &CancellationToken,
|
||||
) {
|
||||
let args = RequestUserInputArgs {
|
||||
questions: event.questions,
|
||||
};
|
||||
let response_fut =
|
||||
parent_session.request_user_input(parent_ctx, parent_ctx.sub_id.clone(), args);
|
||||
let response = await_user_input_with_cancel(
|
||||
response_fut,
|
||||
parent_session,
|
||||
&parent_ctx.sub_id,
|
||||
cancel_token,
|
||||
)
|
||||
.await;
|
||||
let _ = codex.submit(Op::UserInputAnswer { id, response }).await;
|
||||
}
|
||||
|
||||
async fn await_user_input_with_cancel<F>(
|
||||
fut: F,
|
||||
parent_session: &Session,
|
||||
sub_id: &str,
|
||||
cancel_token: &CancellationToken,
|
||||
) -> RequestUserInputResponse
|
||||
where
|
||||
F: core::future::Future<Output = Option<RequestUserInputResponse>>,
|
||||
{
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = cancel_token.cancelled() => {
|
||||
let empty = RequestUserInputResponse {
|
||||
answers: HashMap::new(),
|
||||
};
|
||||
parent_session
|
||||
.notify_user_input_response(sub_id, empty.clone())
|
||||
.await;
|
||||
empty
|
||||
}
|
||||
response = fut => response.unwrap_or_else(|| RequestUserInputResponse {
|
||||
answers: HashMap::new(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// Await an approval decision, aborting on cancellation.
|
||||
async fn await_approval_with_cancel<F>(
|
||||
fut: F,
|
||||
|
||||
@@ -67,6 +67,7 @@ pub(crate) fn should_persist_event_msg(ev: &EventMsg) -> bool {
|
||||
| EventMsg::ExecCommandOutputDelta(_)
|
||||
| EventMsg::ExecCommandEnd(_)
|
||||
| EventMsg::ExecApprovalRequest(_)
|
||||
| EventMsg::RequestUserInput(_)
|
||||
| EventMsg::ElicitationRequest(_)
|
||||
| EventMsg::ApplyPatchApprovalRequest(_)
|
||||
| EventMsg::BackgroundEvent(_)
|
||||
|
||||
@@ -9,6 +9,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use tokio_util::task::AbortOnDropHandle;
|
||||
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
use crate::codex::TurnContext;
|
||||
@@ -68,6 +69,7 @@ impl ActiveTurn {
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TurnState {
|
||||
pending_approvals: HashMap<String, oneshot::Sender<ReviewDecision>>,
|
||||
pending_user_input: HashMap<String, oneshot::Sender<RequestUserInputResponse>>,
|
||||
pending_input: Vec<ResponseInputItem>,
|
||||
}
|
||||
|
||||
@@ -89,9 +91,25 @@ impl TurnState {
|
||||
|
||||
pub(crate) fn clear_pending(&mut self) {
|
||||
self.pending_approvals.clear();
|
||||
self.pending_user_input.clear();
|
||||
self.pending_input.clear();
|
||||
}
|
||||
|
||||
pub(crate) fn insert_pending_user_input(
|
||||
&mut self,
|
||||
key: String,
|
||||
tx: oneshot::Sender<RequestUserInputResponse>,
|
||||
) -> Option<oneshot::Sender<RequestUserInputResponse>> {
|
||||
self.pending_user_input.insert(key, tx)
|
||||
}
|
||||
|
||||
pub(crate) fn remove_pending_user_input(
|
||||
&mut self,
|
||||
key: &str,
|
||||
) -> Option<oneshot::Sender<RequestUserInputResponse>> {
|
||||
self.pending_user_input.remove(key)
|
||||
}
|
||||
|
||||
pub(crate) fn push_pending_input(&mut self, input: ResponseInputItem) {
|
||||
self.pending_input.push(input);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ mod mcp;
|
||||
mod mcp_resource;
|
||||
mod plan;
|
||||
mod read_file;
|
||||
mod request_user_input;
|
||||
mod shell;
|
||||
mod test_sync;
|
||||
mod unified_exec;
|
||||
@@ -23,6 +24,7 @@ pub use mcp::McpHandler;
|
||||
pub use mcp_resource::McpResourceHandler;
|
||||
pub use plan::PlanHandler;
|
||||
pub use read_file::ReadFileHandler;
|
||||
pub use request_user_input::RequestUserInputHandler;
|
||||
pub use shell::ShellCommandHandler;
|
||||
pub use shell::ShellHandler;
|
||||
pub use test_sync::TestSyncHandler;
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
use async_trait::async_trait;
|
||||
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolOutput;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
|
||||
pub struct RequestUserInputHandler;
|
||||
|
||||
#[async_trait]
|
||||
impl ToolHandler for RequestUserInputHandler {
|
||||
fn kind(&self) -> ToolKind {
|
||||
ToolKind::Function
|
||||
}
|
||||
|
||||
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
|
||||
let ToolInvocation {
|
||||
session,
|
||||
turn,
|
||||
call_id,
|
||||
payload,
|
||||
..
|
||||
} = invocation;
|
||||
|
||||
let arguments = match payload {
|
||||
ToolPayload::Function { arguments } => arguments,
|
||||
_ => {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"request_user_input handler received unsupported payload".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let args: RequestUserInputArgs = parse_arguments(&arguments)?;
|
||||
let response = session
|
||||
.request_user_input(turn.as_ref(), call_id, args)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel(
|
||||
"request_user_input was cancelled before receiving a response".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let content = serde_json::to_string(&response).map_err(|err| {
|
||||
FunctionCallError::Fatal(format!(
|
||||
"failed to serialize request_user_input response: {err}"
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(ToolOutput::Function {
|
||||
content,
|
||||
content_items: None,
|
||||
success: Some(true),
|
||||
})
|
||||
}
|
||||
}
|
||||
+165
-10
@@ -27,6 +27,7 @@ pub(crate) struct ToolsConfig {
|
||||
pub apply_patch_tool_type: Option<ApplyPatchToolType>,
|
||||
pub web_search_mode: Option<WebSearchMode>,
|
||||
pub collab_tools: bool,
|
||||
pub collaboration_modes_tools: bool,
|
||||
pub experimental_supported_tools: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -45,6 +46,7 @@ impl ToolsConfig {
|
||||
} = params;
|
||||
let include_apply_patch_tool = features.enabled(Feature::ApplyPatchFreeform);
|
||||
let include_collab_tools = features.enabled(Feature::Collab);
|
||||
let include_collaboration_modes_tools = features.enabled(Feature::CollaborationModes);
|
||||
|
||||
let shell_type = if !features.enabled(Feature::ShellTool) {
|
||||
ConfigShellToolType::Disabled
|
||||
@@ -76,6 +78,7 @@ impl ToolsConfig {
|
||||
apply_patch_tool_type,
|
||||
web_search_mode: *web_search_mode,
|
||||
collab_tools: include_collab_tools,
|
||||
collaboration_modes_tools: include_collaboration_modes_tools,
|
||||
experimental_supported_tools: model_info.experimental_supported_tools.clone(),
|
||||
}
|
||||
}
|
||||
@@ -532,6 +535,88 @@ fn create_wait_tool() -> ToolSpec {
|
||||
})
|
||||
}
|
||||
|
||||
fn create_request_user_input_tool() -> ToolSpec {
|
||||
let mut option_props = BTreeMap::new();
|
||||
option_props.insert(
|
||||
"label".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("User-facing label (1-5 words).".to_string()),
|
||||
},
|
||||
);
|
||||
option_props.insert(
|
||||
"description".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"One short sentence explaining impact/tradeoff if selected.".to_string(),
|
||||
),
|
||||
},
|
||||
);
|
||||
|
||||
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()),
|
||||
},
|
||||
);
|
||||
question_props.insert(
|
||||
"header".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some(
|
||||
"Short header label shown in the UI (12 or fewer chars).".to_string(),
|
||||
),
|
||||
},
|
||||
);
|
||||
question_props.insert(
|
||||
"question".to_string(),
|
||||
JsonSchema::String {
|
||||
description: Some("Single-sentence prompt shown to the user.".to_string()),
|
||||
},
|
||||
);
|
||||
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);
|
||||
|
||||
ToolSpec::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_close_agent_tool() -> ToolSpec {
|
||||
let mut properties = BTreeMap::new();
|
||||
properties.insert(
|
||||
@@ -1140,6 +1225,7 @@ pub(crate) fn build_specs(
|
||||
use crate::tools::handlers::McpResourceHandler;
|
||||
use crate::tools::handlers::PlanHandler;
|
||||
use crate::tools::handlers::ReadFileHandler;
|
||||
use crate::tools::handlers::RequestUserInputHandler;
|
||||
use crate::tools::handlers::ShellCommandHandler;
|
||||
use crate::tools::handlers::ShellHandler;
|
||||
use crate::tools::handlers::TestSyncHandler;
|
||||
@@ -1157,6 +1243,7 @@ pub(crate) fn build_specs(
|
||||
let mcp_handler = Arc::new(McpHandler);
|
||||
let mcp_resource_handler = Arc::new(McpResourceHandler);
|
||||
let shell_command_handler = Arc::new(ShellCommandHandler);
|
||||
let request_user_input_handler = Arc::new(RequestUserInputHandler);
|
||||
|
||||
match &config.shell_type {
|
||||
ConfigShellToolType::Default => {
|
||||
@@ -1197,6 +1284,11 @@ pub(crate) fn build_specs(
|
||||
builder.push_spec(PLAN_TOOL.clone());
|
||||
builder.register_handler("update_plan", plan_handler);
|
||||
|
||||
if config.collaboration_modes_tools {
|
||||
builder.push_spec(create_request_user_input_tool());
|
||||
builder.register_handler("request_user_input", request_user_input_handler);
|
||||
}
|
||||
|
||||
if let Some(apply_patch_tool_type) = &config.apply_patch_tool_type {
|
||||
match apply_patch_tool_type {
|
||||
ApplyPatchToolType::Freeform => {
|
||||
@@ -1398,6 +1490,7 @@ mod tests {
|
||||
let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
features.enable(Feature::CollaborationModes);
|
||||
let config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
features: &features,
|
||||
@@ -1430,6 +1523,7 @@ mod tests {
|
||||
create_list_mcp_resource_templates_tool(),
|
||||
create_read_mcp_resource_tool(),
|
||||
PLAN_TOOL.clone(),
|
||||
create_request_user_input_tool(),
|
||||
create_apply_patch_freeform_tool(),
|
||||
ToolSpec::WebSearch {
|
||||
external_web_access: Some(true),
|
||||
@@ -1460,6 +1554,7 @@ mod tests {
|
||||
let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::Collab);
|
||||
features.enable(Feature::CollaborationModes);
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
features: &features,
|
||||
@@ -1472,6 +1567,33 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_user_input_requires_collaboration_modes_feature() {
|
||||
let config = test_config();
|
||||
let model_info = ModelsManager::construct_model_info_offline("gpt-5-codex", &config);
|
||||
let mut features = Features::with_defaults();
|
||||
features.disable(Feature::CollaborationModes);
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
features: &features,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
});
|
||||
let (tools, _) = build_specs(&tools_config, None).build();
|
||||
assert!(
|
||||
!tools.iter().any(|t| t.spec.name() == "request_user_input"),
|
||||
"request_user_input should be disabled when collaboration_modes feature is off"
|
||||
);
|
||||
|
||||
features.enable(Feature::CollaborationModes);
|
||||
let tools_config = ToolsConfig::new(&ToolsConfigParams {
|
||||
model_info: &model_info,
|
||||
features: &features,
|
||||
web_search_mode: Some(WebSearchMode::Cached),
|
||||
});
|
||||
let (tools, _) = build_specs(&tools_config, None).build();
|
||||
assert_contains_tool_names(&tools, &["request_user_input"]);
|
||||
}
|
||||
|
||||
fn assert_model_tools(
|
||||
model_slug: &str,
|
||||
features: &Features,
|
||||
@@ -1536,9 +1658,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_gpt5_codex_default() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::CollaborationModes);
|
||||
assert_model_tools(
|
||||
"gpt-5-codex",
|
||||
&Features::with_defaults(),
|
||||
&features,
|
||||
Some(WebSearchMode::Cached),
|
||||
&[
|
||||
"shell_command",
|
||||
@@ -1546,6 +1670,7 @@ mod tests {
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
@@ -1555,9 +1680,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_gpt51_codex_default() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::CollaborationModes);
|
||||
assert_model_tools(
|
||||
"gpt-5.1-codex",
|
||||
&Features::with_defaults(),
|
||||
&features,
|
||||
Some(WebSearchMode::Cached),
|
||||
&[
|
||||
"shell_command",
|
||||
@@ -1565,6 +1692,7 @@ mod tests {
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
@@ -1574,9 +1702,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_gpt5_codex_unified_exec_web_search() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
features.enable(Feature::CollaborationModes);
|
||||
assert_model_tools(
|
||||
"gpt-5-codex",
|
||||
Features::with_defaults().enable(Feature::UnifiedExec),
|
||||
&features,
|
||||
Some(WebSearchMode::Live),
|
||||
&[
|
||||
"exec_command",
|
||||
@@ -1585,6 +1716,7 @@ mod tests {
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
@@ -1594,9 +1726,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_build_specs_gpt51_codex_unified_exec_web_search() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
features.enable(Feature::CollaborationModes);
|
||||
assert_model_tools(
|
||||
"gpt-5.1-codex",
|
||||
Features::with_defaults().enable(Feature::UnifiedExec),
|
||||
&features,
|
||||
Some(WebSearchMode::Live),
|
||||
&[
|
||||
"exec_command",
|
||||
@@ -1605,6 +1740,7 @@ mod tests {
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
@@ -1614,9 +1750,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_codex_mini_defaults() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::CollaborationModes);
|
||||
assert_model_tools(
|
||||
"codex-mini-latest",
|
||||
&Features::with_defaults(),
|
||||
&features,
|
||||
Some(WebSearchMode::Cached),
|
||||
&[
|
||||
"local_shell",
|
||||
@@ -1624,6 +1762,7 @@ mod tests {
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"web_search",
|
||||
"view_image",
|
||||
],
|
||||
@@ -1632,9 +1771,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_codex_5_1_mini_defaults() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::CollaborationModes);
|
||||
assert_model_tools(
|
||||
"gpt-5.1-codex-mini",
|
||||
&Features::with_defaults(),
|
||||
&features,
|
||||
Some(WebSearchMode::Cached),
|
||||
&[
|
||||
"shell_command",
|
||||
@@ -1642,6 +1783,7 @@ mod tests {
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
@@ -1651,9 +1793,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_gpt_5_defaults() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::CollaborationModes);
|
||||
assert_model_tools(
|
||||
"gpt-5",
|
||||
&Features::with_defaults(),
|
||||
&features,
|
||||
Some(WebSearchMode::Cached),
|
||||
&[
|
||||
"shell",
|
||||
@@ -1661,6 +1805,7 @@ mod tests {
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"web_search",
|
||||
"view_image",
|
||||
],
|
||||
@@ -1669,9 +1814,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_gpt_5_1_defaults() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::CollaborationModes);
|
||||
assert_model_tools(
|
||||
"gpt-5.1",
|
||||
&Features::with_defaults(),
|
||||
&features,
|
||||
Some(WebSearchMode::Cached),
|
||||
&[
|
||||
"shell_command",
|
||||
@@ -1679,6 +1826,7 @@ mod tests {
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
@@ -1688,9 +1836,11 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_exp_5_1_defaults() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::CollaborationModes);
|
||||
assert_model_tools(
|
||||
"exp-5.1",
|
||||
&Features::with_defaults(),
|
||||
&features,
|
||||
Some(WebSearchMode::Cached),
|
||||
&[
|
||||
"exec_command",
|
||||
@@ -1699,6 +1849,7 @@ mod tests {
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
@@ -1708,9 +1859,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_codex_mini_unified_exec_web_search() {
|
||||
let mut features = Features::with_defaults();
|
||||
features.enable(Feature::UnifiedExec);
|
||||
features.enable(Feature::CollaborationModes);
|
||||
assert_model_tools(
|
||||
"codex-mini-latest",
|
||||
Features::with_defaults().enable(Feature::UnifiedExec),
|
||||
&features,
|
||||
Some(WebSearchMode::Live),
|
||||
&[
|
||||
"exec_command",
|
||||
@@ -1719,6 +1873,7 @@ mod tests {
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"web_search",
|
||||
"view_image",
|
||||
],
|
||||
|
||||
@@ -53,6 +53,7 @@ mod quota_exceeded;
|
||||
mod read_file;
|
||||
mod remote_models;
|
||||
mod request_compression;
|
||||
mod request_user_input;
|
||||
mod resume;
|
||||
mod resume_warning;
|
||||
mod review;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use codex_core::features::Feature;
|
||||
use codex_protocol::config_types::WebSearchMode;
|
||||
use core_test_support::load_sse_fixture_with_id;
|
||||
use core_test_support::responses;
|
||||
@@ -36,7 +37,10 @@ async fn collect_tool_identifiers_for_model(model: &str) -> Vec<String> {
|
||||
let mut builder = test_codex()
|
||||
.with_model(model)
|
||||
// Keep tool expectations stable when the default web_search mode changes.
|
||||
.with_config(|config| config.web_search_mode = Some(WebSearchMode::Cached));
|
||||
.with_config(|config| {
|
||||
config.web_search_mode = Some(WebSearchMode::Cached);
|
||||
config.features.enable(Feature::CollaborationModes);
|
||||
});
|
||||
let test = builder
|
||||
.build(&server)
|
||||
.await
|
||||
@@ -62,6 +66,7 @@ async fn model_selects_expected_tools() {
|
||||
"list_mcp_resource_templates".to_string(),
|
||||
"read_mcp_resource".to_string(),
|
||||
"update_plan".to_string(),
|
||||
"request_user_input".to_string(),
|
||||
"web_search".to_string(),
|
||||
"view_image".to_string()
|
||||
],
|
||||
@@ -77,6 +82,7 @@ async fn model_selects_expected_tools() {
|
||||
"list_mcp_resource_templates".to_string(),
|
||||
"read_mcp_resource".to_string(),
|
||||
"update_plan".to_string(),
|
||||
"request_user_input".to_string(),
|
||||
"apply_patch".to_string(),
|
||||
"web_search".to_string(),
|
||||
"view_image".to_string()
|
||||
@@ -93,6 +99,7 @@ async fn model_selects_expected_tools() {
|
||||
"list_mcp_resource_templates".to_string(),
|
||||
"read_mcp_resource".to_string(),
|
||||
"update_plan".to_string(),
|
||||
"request_user_input".to_string(),
|
||||
"apply_patch".to_string(),
|
||||
"web_search".to_string(),
|
||||
"view_image".to_string()
|
||||
@@ -109,6 +116,7 @@ async fn model_selects_expected_tools() {
|
||||
"list_mcp_resource_templates".to_string(),
|
||||
"read_mcp_resource".to_string(),
|
||||
"update_plan".to_string(),
|
||||
"request_user_input".to_string(),
|
||||
"web_search".to_string(),
|
||||
"view_image".to_string()
|
||||
],
|
||||
@@ -124,6 +132,7 @@ async fn model_selects_expected_tools() {
|
||||
"list_mcp_resource_templates".to_string(),
|
||||
"read_mcp_resource".to_string(),
|
||||
"update_plan".to_string(),
|
||||
"request_user_input".to_string(),
|
||||
"apply_patch".to_string(),
|
||||
"web_search".to_string(),
|
||||
"view_image".to_string()
|
||||
@@ -140,6 +149,7 @@ async fn model_selects_expected_tools() {
|
||||
"list_mcp_resource_templates".to_string(),
|
||||
"read_mcp_resource".to_string(),
|
||||
"update_plan".to_string(),
|
||||
"request_user_input".to_string(),
|
||||
"apply_patch".to_string(),
|
||||
"web_search".to_string(),
|
||||
"view_image".to_string()
|
||||
|
||||
@@ -92,6 +92,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
|
||||
config.model = Some("gpt-5.1-codex-max".to_string());
|
||||
// Keep tool expectations stable when the default web_search mode changes.
|
||||
config.web_search_mode = Some(WebSearchMode::Cached);
|
||||
config.features.enable(Feature::CollaborationModes);
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
@@ -135,6 +136,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> {
|
||||
"list_mcp_resource_templates",
|
||||
"read_mcp_resource",
|
||||
"update_plan",
|
||||
"request_user_input",
|
||||
"apply_patch",
|
||||
"web_search",
|
||||
"view_image",
|
||||
@@ -176,6 +178,7 @@ async fn codex_mini_latest_tools() -> anyhow::Result<()> {
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some("be consistent and helpful".to_string());
|
||||
config.features.disable(Feature::ApplyPatchFreeform);
|
||||
config.features.enable(Feature::CollaborationModes);
|
||||
config.model = Some("codex-mini-latest".to_string());
|
||||
})
|
||||
.build(&server)
|
||||
@@ -240,6 +243,7 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
|
||||
let TestCodex { codex, config, .. } = test_codex()
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some("be consistent and helpful".to_string());
|
||||
config.features.enable(Feature::CollaborationModes);
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
@@ -316,6 +320,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an
|
||||
let TestCodex { codex, .. } = test_codex()
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some("be consistent and helpful".to_string());
|
||||
config.features.enable(Feature::CollaborationModes);
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
@@ -538,6 +543,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res
|
||||
let TestCodex { codex, .. } = test_codex()
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some("be consistent and helpful".to_string());
|
||||
config.features.enable(Feature::CollaborationModes);
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
@@ -645,6 +651,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a
|
||||
} = test_codex()
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some("be consistent and helpful".to_string());
|
||||
config.features.enable(Feature::CollaborationModes);
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
@@ -742,6 +749,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu
|
||||
} = test_codex()
|
||||
.with_config(|config| {
|
||||
config.user_instructions = Some("be consistent and helpful".to_string());
|
||||
config.features.enable(Feature::CollaborationModes);
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_core::features::Feature;
|
||||
use codex_core::protocol::AskForApproval;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::request_user_input::RequestUserInputAnswer;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::responses::ResponsesRequest;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use core_test_support::wait_for_event_match;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
|
||||
fn call_output(req: &ResponsesRequest, call_id: &str) -> String {
|
||||
let raw = req.function_call_output(call_id);
|
||||
assert_eq!(
|
||||
raw.get("call_id").and_then(Value::as_str),
|
||||
Some(call_id),
|
||||
"mismatched call_id in function_call_output"
|
||||
);
|
||||
let (content_opt, _success) = match req.function_call_output_content_and_success(call_id) {
|
||||
Some(values) => values,
|
||||
None => panic!("function_call_output present"),
|
||||
};
|
||||
match content_opt {
|
||||
Some(content) => content,
|
||||
None => panic!("function_call_output content present"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn request_user_input_round_trip_resolves_pending() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
|
||||
let builder = test_codex();
|
||||
let TestCodex {
|
||||
codex,
|
||||
cwd,
|
||||
session_configured,
|
||||
..
|
||||
} = builder
|
||||
.with_config(|config| {
|
||||
config.features.enable(Feature::CollaborationModes);
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
let call_id = "user-input-call";
|
||||
let request_args = json!({
|
||||
"questions": [{
|
||||
"id": "confirm_path",
|
||||
"header": "Confirm",
|
||||
"question": "Proceed with the plan?",
|
||||
"options": [{
|
||||
"label": "Yes (Recommended)",
|
||||
"description": "Continue the current plan."
|
||||
}, {
|
||||
"label": "No",
|
||||
"description": "Stop and revisit the approach."
|
||||
}]
|
||||
}]
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let first_response = sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_function_call(call_id, "request_user_input", &request_args),
|
||||
ev_completed("resp-1"),
|
||||
]);
|
||||
responses::mount_sse_once(&server, first_response).await;
|
||||
|
||||
let second_response = sse(vec![
|
||||
ev_assistant_message("msg-1", "thanks"),
|
||||
ev_completed("resp-2"),
|
||||
]);
|
||||
let second_mock = responses::mount_sse_once(&server, second_response).await;
|
||||
|
||||
let session_model = session_configured.model.clone();
|
||||
|
||||
codex
|
||||
.submit(Op::UserTurn {
|
||||
items: vec![UserInput::Text {
|
||||
text: "please confirm".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_model,
|
||||
effort: None,
|
||||
summary: ReasoningSummary::Auto,
|
||||
collaboration_mode: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
let request = wait_for_event_match(&codex, |event| match event {
|
||||
EventMsg::RequestUserInput(request) => Some(request.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(request.call_id, call_id);
|
||||
assert_eq!(request.questions.len(), 1);
|
||||
|
||||
let mut answers = HashMap::new();
|
||||
answers.insert(
|
||||
"confirm_path".to_string(),
|
||||
RequestUserInputAnswer {
|
||||
selected: vec!["yes".to_string()],
|
||||
other: None,
|
||||
},
|
||||
);
|
||||
let response = RequestUserInputResponse { answers };
|
||||
codex
|
||||
.submit(Op::UserInputAnswer {
|
||||
id: request.turn_id.clone(),
|
||||
response,
|
||||
})
|
||||
.await?;
|
||||
|
||||
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let req = second_mock.single_request();
|
||||
let output_text = call_output(&req, call_id);
|
||||
let output_json: Value = serde_json::from_str(&output_text)?;
|
||||
assert_eq!(
|
||||
output_json,
|
||||
json!({
|
||||
"answers": {
|
||||
"confirm_path": { "selected": ["yes"], "other": Value::Null }
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -28,7 +28,6 @@ use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
|
||||
fn call_output(req: &ResponsesRequest, call_id: &str) -> (String, Option<bool>) {
|
||||
let raw = req.function_call_output(call_id);
|
||||
assert_eq!(
|
||||
|
||||
@@ -68,10 +68,12 @@ For complete documentation of the `Op` and `EventMsg` variants, refer to [protoc
|
||||
- `Op::UserInput` – Any input from the user to kick off a `Turn`
|
||||
- `Op::Interrupt` – Interrupts a running turn
|
||||
- `Op::ExecApproval` – Approve or deny code execution
|
||||
- `Op::UserInputAnswer` – Provide answers for a `request_user_input` tool call
|
||||
- `Op::ListSkills` – Request skills for one or more cwd values (optionally `force_reload`)
|
||||
- `EventMsg`
|
||||
- `EventMsg::AgentMessage` – Messages from the `Model`
|
||||
- `EventMsg::ExecApprovalRequest` – Request approval from user to execute a command
|
||||
- `EventMsg::RequestUserInput` – Request user input for a tool call
|
||||
- `EventMsg::TurnComplete` – A turn completed successfully
|
||||
- `EventMsg::Error` – A turn stopped with an error
|
||||
- `EventMsg::Warning` – A non-fatal warning that the client should surface to the user
|
||||
|
||||
@@ -606,7 +606,8 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
| EventMsg::SkillsUpdateAvailable
|
||||
| EventMsg::UndoCompleted(_)
|
||||
| EventMsg::UndoStarted(_)
|
||||
| EventMsg::ThreadRolledBack(_) => {}
|
||||
| EventMsg::ThreadRolledBack(_)
|
||||
| EventMsg::RequestUserInput(_) => {}
|
||||
}
|
||||
CodexStatus::Running
|
||||
}
|
||||
|
||||
@@ -358,6 +358,7 @@ async fn run_codex_tool_session_inner(
|
||||
| EventMsg::UndoStarted(_)
|
||||
| EventMsg::UndoCompleted(_)
|
||||
| EventMsg::ExitedReviewMode(_)
|
||||
| EventMsg::RequestUserInput(_)
|
||||
| EventMsg::ContextCompacted(_)
|
||||
| EventMsg::ThreadRolledBack(_)
|
||||
| EventMsg::CollabAgentSpawnBegin(_)
|
||||
|
||||
@@ -137,8 +137,10 @@ impl McpProcess {
|
||||
|
||||
let initialized = self.read_jsonrpc_message().await?;
|
||||
let os_info = os_info::get();
|
||||
let build_version = env!("CARGO_PKG_VERSION");
|
||||
let originator = codex_core::default_client::originator().value;
|
||||
let user_agent = format!(
|
||||
"codex_cli_rs/0.0.0 ({} {}; {}) {} (elicitation test; 0.0.0)",
|
||||
"{originator}/{build_version} ({} {}; {}) {} (elicitation test; 0.0.0)",
|
||||
os_info.os_type(),
|
||||
os_info.version(),
|
||||
os_info.architecture().unwrap_or("unknown"),
|
||||
|
||||
@@ -14,4 +14,5 @@ pub mod openai_models;
|
||||
pub mod parse_command;
|
||||
pub mod plan_tool;
|
||||
pub mod protocol;
|
||||
pub mod request_user_input;
|
||||
pub mod user_input;
|
||||
|
||||
@@ -24,6 +24,7 @@ use crate::num_format::format_with_separators;
|
||||
use crate::openai_models::ReasoningEffort as ReasoningEffortConfig;
|
||||
use crate::parse_command::ParsedCommand;
|
||||
use crate::plan_tool::UpdatePlanArgs;
|
||||
use crate::request_user_input::RequestUserInputResponse;
|
||||
use crate::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use mcp_types::CallToolResult;
|
||||
@@ -44,6 +45,7 @@ pub use crate::approvals::ApplyPatchApprovalRequestEvent;
|
||||
pub use crate::approvals::ElicitationAction;
|
||||
pub use crate::approvals::ExecApprovalRequestEvent;
|
||||
pub use crate::approvals::ExecPolicyAmendment;
|
||||
pub use crate::request_user_input::RequestUserInputEvent;
|
||||
|
||||
/// Open/close tags for special user-input blocks. Used across crates to avoid
|
||||
/// duplicated hardcoded strings.
|
||||
@@ -191,6 +193,15 @@ pub enum Op {
|
||||
decision: ElicitationAction,
|
||||
},
|
||||
|
||||
/// Resolve a request_user_input tool call.
|
||||
#[serde(rename = "user_input_answer", alias = "request_user_input_response")]
|
||||
UserInputAnswer {
|
||||
/// Turn id for the in-flight request.
|
||||
id: String,
|
||||
/// User-provided answers.
|
||||
response: RequestUserInputResponse,
|
||||
},
|
||||
|
||||
/// Append an entry to the persistent cross-session message history.
|
||||
///
|
||||
/// Note the entry is not guaranteed to be logged if the user has
|
||||
@@ -723,6 +734,8 @@ pub enum EventMsg {
|
||||
|
||||
ExecApprovalRequest(ExecApprovalRequestEvent),
|
||||
|
||||
RequestUserInput(RequestUserInputEvent),
|
||||
|
||||
ElicitationRequest(ElicitationRequestEvent),
|
||||
|
||||
ApplyPatchApprovalRequest(ApplyPatchApprovalRequestEvent),
|
||||
|
||||
@@ -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 selected: Vec<String>,
|
||||
pub other: Option<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>,
|
||||
}
|
||||
@@ -2547,7 +2547,8 @@ impl ChatWidget {
|
||||
| EventMsg::ItemCompleted(_)
|
||||
| EventMsg::AgentMessageContentDelta(_)
|
||||
| EventMsg::ReasoningContentDelta(_)
|
||||
| EventMsg::ReasoningRawContentDelta(_) => {}
|
||||
| EventMsg::ReasoningRawContentDelta(_)
|
||||
| EventMsg::RequestUserInput(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2318,7 +2318,8 @@ impl ChatWidget {
|
||||
| EventMsg::ItemCompleted(_)
|
||||
| EventMsg::AgentMessageContentDelta(_)
|
||||
| EventMsg::ReasoningContentDelta(_)
|
||||
| EventMsg::ReasoningRawContentDelta(_) => {}
|
||||
| EventMsg::ReasoningRawContentDelta(_)
|
||||
| EventMsg::RequestUserInput(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user