mirror of
https://github.com/openai/codex.git
synced 2026-04-30 01:16:54 +00:00
feat(app-server): turn/steer API (#10821)
This PR adds a dedicated `turn/steer` API for appending user input to an in-flight turn. ## Motivation Currently, steering in the app is implemented by just calling `turn/start` while a turn is running. This has some really weird quirks: - Client gets back a new `turn.id`, even though streamed events/approvals remained tied to the original active turn ID. - All the various turn-level override params on `turn/start` do not apply to the "steer", and would only apply to the next real turn. - There can also be a race condition where the client thinks the turn is active but the server has already completed it, so there might be bugs if the client has baked in some client-specific behavior thinking it's a steer when in fact the server kicked off a new turn. This is particularly possible when running a client against a remote app-server. Having a dedicated `turn/steer` API eliminates all those quirks. `turn/steer` behavior: - Requires an active turn on threadId. Returns a JSON-RPC error if there is no active turn. - If expectedTurnId is provided, it must match the active turn (more useful when connecting to a remote app-server). - Does not emit `turn/started`. - Does not accept turn overrides (`cwd`, `model`, `sandbox`, etc.) or `outputSchema` to accurately reflect that these are not applied when steering.
This commit is contained in:
@@ -139,6 +139,8 @@ use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnStartedNotification;
|
||||
use codex_app_server_protocol::TurnStatus;
|
||||
use codex_app_server_protocol::TurnSteerParams;
|
||||
use codex_app_server_protocol::TurnSteerResponse;
|
||||
use codex_app_server_protocol::UserInfoResponse;
|
||||
use codex_app_server_protocol::UserInput as V2UserInput;
|
||||
use codex_app_server_protocol::UserSavedConfig;
|
||||
@@ -154,6 +156,7 @@ use codex_core::InitialHistory;
|
||||
use codex_core::NewThread;
|
||||
use codex_core::RolloutRecorder;
|
||||
use codex_core::SessionMeta;
|
||||
use codex_core::SteerInputError;
|
||||
use codex_core::ThreadConfigSnapshot;
|
||||
use codex_core::ThreadManager;
|
||||
use codex_core::ThreadSortKey as CoreThreadSortKey;
|
||||
@@ -532,6 +535,10 @@ impl CodexMessageProcessor {
|
||||
self.turn_start(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::TurnSteer { request_id, params } => {
|
||||
self.turn_steer(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::TurnInterrupt { request_id, params } => {
|
||||
self.turn_interrupt(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
@@ -4620,6 +4627,63 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn turn_steer(&self, request_id: ConnectionRequestId, params: TurnSteerParams) {
|
||||
let (_, thread) = match self.load_thread(¶ms.thread_id).await {
|
||||
Ok(v) => v,
|
||||
Err(error) => {
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if params.expected_turn_id.is_empty() {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
"expectedTurnId must not be empty".to_string(),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
let mapped_items: Vec<CoreInputItem> = params
|
||||
.input
|
||||
.into_iter()
|
||||
.map(V2UserInput::into_core)
|
||||
.collect();
|
||||
|
||||
match thread
|
||||
.steer_input(mapped_items, Some(¶ms.expected_turn_id))
|
||||
.await
|
||||
{
|
||||
Ok(turn_id) => {
|
||||
let response = TurnSteerResponse { turn_id };
|
||||
self.outgoing.send_response(request_id, response).await;
|
||||
}
|
||||
Err(err) => {
|
||||
let (code, message) = match err {
|
||||
SteerInputError::NoActiveTurn(_) => (
|
||||
INVALID_REQUEST_ERROR_CODE,
|
||||
"no active turn to steer".to_string(),
|
||||
),
|
||||
SteerInputError::ExpectedTurnMismatch { expected, actual } => (
|
||||
INVALID_REQUEST_ERROR_CODE,
|
||||
format!("expected active turn id `{expected}` but found `{actual}`"),
|
||||
),
|
||||
SteerInputError::EmptyInput => (
|
||||
INVALID_REQUEST_ERROR_CODE,
|
||||
"input must not be empty".to_string(),
|
||||
),
|
||||
};
|
||||
let error = JSONRPCErrorError {
|
||||
code,
|
||||
message,
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_review_turn(turn_id: String, display_text: &str) -> Turn {
|
||||
let items = if display_text.is_empty() {
|
||||
Vec::new()
|
||||
|
||||
Reference in New Issue
Block a user