Files
codex/codex-rs/ext/agent/src/lib.rs
Owen Lin cbb7e82a8b Unify turn input submission and routing (#38275)
## What changed

- Add `TurnInputRequest` and typed submission results for atomically starting a turn, steering the active turn, or declining input with a specific reason.
- Expose `start_or_steer_turn`, `start_turn_if_idle`, and `steer_turn` on `CodexThread`, and migrate Core consumers to these APIs.
- Make app-server `turn/start` steer an active regular turn and return that turn's ID. Reject incompatible output schemas and non-steerable turns without applying settings or enqueueing input.

## Testing

- Cover concurrent start-or-steer submissions, accepted and rejected settings updates, output-schema compatibility, idle-start rejection, and app-server steering.

GitOrigin-RevId: dd9b5528d76ec650c019e97af420bc13190ea86a
2026-08-13 00:00:16 +00:00

101 lines
3.0 KiB
Rust

use codex_core::CodexThread;
use codex_core::NewThread;
use codex_core::StartIfIdleSubmission;
use codex_core::StartThreadOptions;
use codex_core::ThreadManager;
use codex_core::TurnInputRequest;
use codex_core::config::Config;
use codex_protocol::ThreadId;
use codex_protocol::error::CodexErr;
use codex_protocol::error::Result as CodexResult;
use codex_protocol::protocol::W3cTraceContext;
use codex_protocol::user_input::UserInput;
use std::sync::Arc;
use std::sync::Weak;
/// A fully resolved agent invocation.
///
/// Agent discovery owns rendering `prompt`, including any selected skill
/// references. The runtime only starts that prompt in isolated forked context.
pub struct AgentInvocation {
pub config: Config,
pub prompt: String,
pub parent_trace: Option<W3cTraceContext>,
}
/// A spawned agent whose initial turn has been submitted.
pub struct AgentRun {
pub thread_id: ThreadId,
pub turn_id: String,
pub thread: Arc<CodexThread>,
}
/// Runs resolved agents in threads forked by the owning [`ThreadManager`].
#[derive(Clone)]
pub struct AgentRunner {
thread_manager: Weak<ThreadManager>,
}
impl AgentRunner {
pub fn new(thread_manager: Weak<ThreadManager>) -> Self {
Self { thread_manager }
}
/// Starts a resolved agent in a fork of `parent_thread_id`.
pub async fn start(
&self,
parent_thread_id: ThreadId,
invocation: AgentInvocation,
) -> CodexResult<AgentRun> {
let AgentInvocation {
config,
prompt,
parent_trace,
} = invocation;
if prompt.trim().is_empty() {
return Err(CodexErr::InvalidRequest(
"agent prompt must not be empty".to_string(),
));
}
let thread_manager = self
.thread_manager
.upgrade()
.ok_or_else(|| CodexErr::UnsupportedOperation("thread manager dropped".to_string()))?;
let NewThread {
thread_id, thread, ..
} = thread_manager
.spawn_subagent(
parent_thread_id,
StartThreadOptions {
parent_trace: parent_trace.clone(),
..StartThreadOptions::new(config)
},
)
.await?;
let turn_id = match thread
.start_turn_if_idle(
TurnInputRequest::user_input(vec![UserInput::Text {
text: prompt,
text_elements: Vec::new(),
}])
.with_trace(parent_trace),
)
.await?
{
StartIfIdleSubmission::Started { turn_id } => turn_id,
StartIfIdleSubmission::NotSubmitted { reason } => {
return Err(CodexErr::InvalidRequest(format!(
"agent prompt was not submitted: {reason:?}"
)));
}
};
Ok(AgentRun {
thread_id,
turn_id,
thread,
})
}
}