code-mode: introduce durable session interface (#24180)

## Summary

Introduce a `CodeModeSession` interface for executing and managing
code-mode cells.

This moves cell lifecycle, callback delegation, termination, and
shutdown behind a session abstraction, while continuing to use the
existing in-process implementation, and the ability to implement an
external process one behind this interface.

A Codex session owns one `CodeModeSession`, which in turn owns its
running cells and stored code-mode state. Each cell is represented to
the caller as a `StartedCell`, exposing its cell ID and initial
response.

It also introduces a `CodeModeSessionDelegate` callback interface. A
session uses the delegate to invoke nested host tools and emit
notifications while a cell is running, allowing the runtime to
communicate with its owning Codex session without depending directly on
core turn handling.

<img width="2121" height="1001" alt="image"
src="https://github.com/user-attachments/assets/c349a819-2a59-485c-bda4-2caf68ac4c31"
/>
This commit is contained in:
Channing Conger authored and GitHub committed 2026-05-29 11:42:52 -07:00
1 parent 451b386442
commit c9dc0f6338
11 files changed
+1036 -438

No files matched your search

-2
View File
@@ -2430,8 +2430,6 @@ dependencies = [
name = "codex-code-mode" name = "codex-code-mode"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"async-channel",
"async-trait",
"codex-protocol", "codex-protocol",
"deno_core_icudata", "deno_core_icudata",
"pretty_assertions", "pretty_assertions",
-2
View File
@@ -16,8 +16,6 @@ sandbox = ["v8/v8_enable_sandbox"]
workspace = true workspace = true
[dependencies] [dependencies]
async-channel = { workspace = true }
async-trait = { workspace = true }
codex-protocol = { workspace = true } codex-protocol = { workspace = true }
deno_core_icudata = { workspace = true } deno_core_icudata = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
+11 -2
View File
@@ -29,9 +29,18 @@ pub use runtime::WaitOutcome;
pub use runtime::WaitRequest; pub use runtime::WaitRequest;
pub use runtime::WaitToPendingOutcome; pub use runtime::WaitToPendingOutcome;
pub use runtime::WaitToPendingRequest; pub use runtime::WaitToPendingRequest;
pub use service::CellId;
pub use service::CodeModeService; pub use service::CodeModeService;
pub use service::CodeModeTurnHost; pub use service::CodeModeSession;
pub use service::CodeModeTurnWorker; pub use service::CodeModeSessionDelegate;
pub use service::CodeModeSessionProvider;
pub use service::CodeModeSessionProviderFuture;
pub use service::CodeModeSessionResultFuture;
pub use service::InProcessCodeModeSessionProvider;
pub use service::NoopCodeModeSessionDelegate;
pub use service::NotificationFuture;
pub use service::StartedCell;
pub use service::ToolInvocationFuture;
pub const PUBLIC_TOOL_NAME: &str = "exec"; pub const PUBLIC_TOOL_NAME: &str = "exec";
pub const WAIT_TOOL_NAME: &str = "wait"; pub const WAIT_TOOL_NAME: &str = "wait";
+8 -24
View File
@@ -19,6 +19,7 @@ use crate::description::EnabledToolMetadata;
use crate::description::ToolDefinition; use crate::description::ToolDefinition;
use crate::description::enabled_tool_metadata; use crate::description::enabled_tool_metadata;
use crate::response::FunctionCallOutputContentItem; use crate::response::FunctionCallOutputContentItem;
use crate::service::CellId;
pub const DEFAULT_EXEC_YIELD_TIME_MS: u64 = 10_000; pub const DEFAULT_EXEC_YIELD_TIME_MS: u64 = 10_000;
pub const DEFAULT_WAIT_YIELD_TIME_MS: u64 = 10_000; pub const DEFAULT_WAIT_YIELD_TIME_MS: u64 = 10_000;
@@ -27,11 +28,6 @@ const EXIT_SENTINEL: &str = "__codex_code_mode_exit__";
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ExecuteRequest { pub struct ExecuteRequest {
/// Runtime cell id for this execution.
///
/// Callers allocate this before execution so tracing, waits, and nested tool
/// calls can refer to the cell as soon as JavaScript starts.
pub cell_id: String,
pub tool_call_id: String, pub tool_call_id: String,
pub enabled_tools: Vec<ToolDefinition>, pub enabled_tools: Vec<ToolDefinition>,
pub source: String, pub source: String,
@@ -41,14 +37,13 @@ pub struct ExecuteRequest {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct WaitRequest { pub struct WaitRequest {
pub cell_id: String, pub cell_id: CellId,
pub yield_time_ms: u64, pub yield_time_ms: u64,
pub terminate: bool,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct WaitToPendingRequest { pub struct WaitToPendingRequest {
pub cell_id: String, pub cell_id: CellId,
} }
/// Result of waiting on a code-mode cell. /// Result of waiting on a code-mode cell.
@@ -73,7 +68,7 @@ pub enum ExecuteToPendingOutcome {
/// The cell is waiting for more runtime input after draining the runtime /// The cell is waiting for more runtime input after draining the runtime
/// input queue that was ready at the pending boundary. /// input queue that was ready at the pending boundary.
Pending { Pending {
cell_id: String, cell_id: CellId,
content_items: Vec<FunctionCallOutputContentItem>, content_items: Vec<FunctionCallOutputContentItem>,
/// Runtime tool-call ids emitted before this paused execution frontier /// Runtime tool-call ids emitted before this paused execution frontier
/// sealed. Hosts can use these ids to drain their tool-call transport /// sealed. Hosts can use these ids to drain their tool-call transport
@@ -105,15 +100,15 @@ impl From<WaitOutcome> for RuntimeResponse {
#[derive(Debug, PartialEq, Serialize)] #[derive(Debug, PartialEq, Serialize)]
pub enum RuntimeResponse { pub enum RuntimeResponse {
Yielded { Yielded {
cell_id: String, cell_id: CellId,
content_items: Vec<FunctionCallOutputContentItem>, content_items: Vec<FunctionCallOutputContentItem>,
}, },
Terminated { Terminated {
cell_id: String, cell_id: CellId,
content_items: Vec<FunctionCallOutputContentItem>, content_items: Vec<FunctionCallOutputContentItem>,
}, },
Result { Result {
cell_id: String, cell_id: CellId,
content_items: Vec<FunctionCallOutputContentItem>, content_items: Vec<FunctionCallOutputContentItem>,
error_text: Option<String>, error_text: Option<String>,
}, },
@@ -126,23 +121,13 @@ pub enum RuntimeResponse {
/// if their tool-call graph requires globally unique ids. /// if their tool-call graph requires globally unique ids.
#[derive(Debug)] #[derive(Debug)]
pub struct CodeModeNestedToolCall { pub struct CodeModeNestedToolCall {
pub cell_id: String, pub cell_id: CellId,
pub runtime_tool_call_id: String, pub runtime_tool_call_id: String,
pub tool_name: ToolName, pub tool_name: ToolName,
pub tool_kind: CodeModeToolKind, pub tool_kind: CodeModeToolKind,
pub input: Option<JsonValue>, pub input: Option<JsonValue>,
} }
#[derive(Debug)]
pub(crate) enum TurnMessage {
ToolCall(CodeModeNestedToolCall),
Notify {
cell_id: String,
call_id: String,
text: String,
},
}
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum RuntimeCommand { pub(crate) enum RuntimeCommand {
ToolResponse { id: String, result: JsonValue }, ToolResponse { id: String, result: JsonValue },
@@ -460,7 +445,6 @@ mod tests {
fn execute_request(source: &str) -> ExecuteRequest { fn execute_request(source: &str) -> ExecuteRequest {
ExecuteRequest { ExecuteRequest {
cell_id: "1".to_string(),
tool_call_id: "call_1".to_string(), tool_call_id: "call_1".to_string(),
enabled_tools: Vec::new(), enabled_tools: Vec::new(),
source: source.to_string(), source: source.to_string(),
File diff suppressed because it is too large. Load diff
+3
View File
@@ -630,6 +630,9 @@ async fn shutdown_session_runtime(sess: &Arc<Session>) {
.unified_exec_manager .unified_exec_manager
.terminate_all_processes() .terminate_all_processes()
.await; .await;
if let Err(err) = sess.services.code_mode_service.shutdown().await {
warn!("failed to shutdown code mode session: {err}");
}
let mcp_shutdown = { let mcp_shutdown = {
let mut manager = sess.services.mcp_connection_manager.write().await; let mut manager = sess.services.mcp_connection_manager.write().await;
manager.begin_shutdown() manager.begin_shutdown()
+2 -6
View File
@@ -943,16 +943,12 @@ async fn run_sampling_request(
Arc::clone(&turn_context), Arc::clone(&turn_context),
Arc::clone(&turn_diff_tracker), Arc::clone(&turn_diff_tracker),
); );
let _code_mode_worker = sess let _code_mode_worker = sess.services.code_mode_service.start_turn_worker(
.services
.code_mode_service
.start_turn_worker(
&sess, &sess,
&turn_context, &turn_context,
Arc::clone(&router), Arc::clone(&router),
Arc::clone(&turn_diff_tracker), Arc::clone(&turn_diff_tracker),
) );
.await;
let max_retries = turn_context.provider.info().stream_max_retries(); let max_retries = turn_context.provider.info().stream_max_retries();
let mut retries = 0; let mut retries = 0;
let mut initial_input = Some(input); let mut initial_input = Some(input);
@@ -0,0 +1,311 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
use codex_code_mode::CellId;
use codex_code_mode::CodeModeNestedToolCall;
use codex_code_mode::CodeModeSessionDelegate;
use codex_code_mode::NotificationFuture;
use codex_code_mode::ToolInvocationFuture;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseItem;
use serde_json::Value as JsonValue;
use tokio::sync::oneshot;
use tokio::sync::watch;
use tokio_util::sync::CancellationToken;
use super::ExecContext;
use super::PUBLIC_TOOL_NAME;
use super::call_nested_tool;
use crate::tools::ToolRouter;
use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::parallel::ToolCallRuntime;
pub(super) struct CodeModeDispatchBroker {
dispatch_tx: async_channel::Sender<DispatchMessage>,
dispatch_rx: async_channel::Receiver<DispatchMessage>,
dispatch_gates: Arc<Mutex<HashMap<CellId, watch::Sender<bool>>>>,
}
impl CodeModeDispatchBroker {
pub(super) fn new() -> Self {
let (dispatch_tx, dispatch_rx) = async_channel::unbounded();
Self {
dispatch_tx,
dispatch_rx,
dispatch_gates: Arc::new(Mutex::new(HashMap::new())),
}
}
pub(super) fn mark_cell_ready_for_dispatch(&self, cell_id: &CellId) {
dispatch_gate(&self.dispatch_gates, cell_id).send_replace(true);
}
pub(super) fn close_cell(&self, cell_id: &CellId) {
remove_dispatch_gate(&self.dispatch_gates, cell_id);
}
pub(super) fn start_turn_worker(
&self,
exec: ExecContext,
router: Arc<ToolRouter>,
tracker: SharedTurnDiffTracker,
) -> CodeModeDispatchWorker {
let tool_runtime = ToolCallRuntime::new(
router,
Arc::clone(&exec.session),
Arc::clone(&exec.turn),
tracker,
);
let host = Arc::new(CoreTurnHost { exec, tool_runtime });
let dispatch_rx = self.dispatch_rx.clone();
let dispatch_gates = Arc::clone(&self.dispatch_gates);
let (shutdown_tx, mut shutdown_rx) = oneshot::channel();
tokio::spawn(async move {
loop {
let message = tokio::select! {
_ = &mut shutdown_rx => break,
message = dispatch_rx.recv() => message.ok(),
};
let Some(message) = message else {
break;
};
match message {
DispatchMessage::Notify {
call_id,
cell_id,
text,
cancellation_token,
response_tx,
} => {
let response = if wait_until_cell_ready_for_dispatch(
&dispatch_gates,
&cell_id,
&cancellation_token,
)
.await
{
host.notify(call_id, cell_id, text).await
} else {
remove_dispatch_gate(&dispatch_gates, &cell_id);
Err("code mode notification cancelled".to_string())
};
let _ = response_tx.send(response);
}
DispatchMessage::InvokeTool {
invocation,
cancellation_token,
response_tx,
} => {
let cell_id = invocation.cell_id.clone();
if !wait_until_cell_ready_for_dispatch(
&dispatch_gates,
&cell_id,
&cancellation_token,
)
.await
{
remove_dispatch_gate(&dispatch_gates, &cell_id);
continue;
}
let host = Arc::clone(&host);
tokio::spawn(async move {
let response = tokio::select! {
response = host.invoke_tool(
invocation,
cancellation_token.clone(),
) => response,
_ = cancellation_token.cancelled() => return,
};
let _ = response_tx.send(response);
});
}
}
}
});
CodeModeDispatchWorker {
shutdown_tx: Some(shutdown_tx),
}
}
}
fn dispatch_gate(
dispatch_gates: &Mutex<HashMap<CellId, watch::Sender<bool>>>,
cell_id: &CellId,
) -> watch::Sender<bool> {
let mut dispatch_gates = match dispatch_gates.lock() {
Ok(dispatch_gates) => dispatch_gates,
Err(poisoned) => poisoned.into_inner(),
};
dispatch_gates
.entry(cell_id.clone())
.or_insert_with(|| watch::channel(false).0)
.clone()
}
fn remove_dispatch_gate(
dispatch_gates: &Mutex<HashMap<CellId, watch::Sender<bool>>>,
cell_id: &CellId,
) {
let mut dispatch_gates = match dispatch_gates.lock() {
Ok(dispatch_gates) => dispatch_gates,
Err(poisoned) => poisoned.into_inner(),
};
dispatch_gates.remove(cell_id);
}
async fn wait_until_cell_ready_for_dispatch(
dispatch_gates: &Mutex<HashMap<CellId, watch::Sender<bool>>>,
cell_id: &CellId,
cancellation_token: &CancellationToken,
) -> bool {
if cancellation_token.is_cancelled() {
return false;
}
let mut ready_rx = dispatch_gate(dispatch_gates, cell_id).subscribe();
loop {
if *ready_rx.borrow_and_update() {
return true;
}
tokio::select! {
changed = ready_rx.changed() => {
if changed.is_err() {
return false;
}
}
_ = cancellation_token.cancelled() => return false,
}
}
}
impl CodeModeSessionDelegate for CodeModeDispatchBroker {
fn invoke_tool<'a>(
&'a self,
invocation: CodeModeNestedToolCall,
cancellation_token: CancellationToken,
) -> ToolInvocationFuture<'a> {
Box::pin(async move {
if cancellation_token.is_cancelled() {
return Err("code mode nested tool call cancelled".to_string());
}
let (response_tx, response_rx) = oneshot::channel();
self.dispatch_tx
.send(DispatchMessage::InvokeTool {
invocation,
cancellation_token: cancellation_token.clone(),
response_tx,
})
.await
.map_err(|_| "code mode nested tool dispatcher is unavailable".to_string())?;
tokio::select! {
response = response_rx => response
.map_err(|_| "code mode nested tool dispatcher stopped".to_string())?,
_ = cancellation_token.cancelled() => {
Err("code mode nested tool call cancelled".to_string())
}
}
})
}
fn notify<'a>(
&'a self,
call_id: String,
cell_id: CellId,
text: String,
cancellation_token: CancellationToken,
) -> NotificationFuture<'a> {
Box::pin(async move {
if cancellation_token.is_cancelled() {
return Err("code mode notification cancelled".to_string());
}
let (response_tx, response_rx) = oneshot::channel();
self.dispatch_tx
.send(DispatchMessage::Notify {
call_id,
cell_id,
text,
cancellation_token: cancellation_token.clone(),
response_tx,
})
.await
.map_err(|_| "code mode notification dispatcher is unavailable".to_string())?;
tokio::select! {
response = response_rx => response
.map_err(|_| "code mode notification dispatcher stopped".to_string())?,
_ = cancellation_token.cancelled() => {
Err("code mode notification cancelled".to_string())
}
}
})
}
fn cell_closed(&self, cell_id: &CellId) {
self.close_cell(cell_id);
}
}
enum DispatchMessage {
InvokeTool {
invocation: CodeModeNestedToolCall,
cancellation_token: CancellationToken,
response_tx: oneshot::Sender<Result<JsonValue, String>>,
},
Notify {
call_id: String,
cell_id: CellId,
text: String,
cancellation_token: CancellationToken,
response_tx: oneshot::Sender<Result<(), String>>,
},
}
pub(crate) struct CodeModeDispatchWorker {
shutdown_tx: Option<oneshot::Sender<()>>,
}
impl Drop for CodeModeDispatchWorker {
fn drop(&mut self) {
if let Some(shutdown_tx) = self.shutdown_tx.take() {
let _ = shutdown_tx.send(());
}
}
}
struct CoreTurnHost {
exec: ExecContext,
tool_runtime: ToolCallRuntime,
}
impl CoreTurnHost {
async fn invoke_tool(
&self,
invocation: CodeModeNestedToolCall,
cancellation_token: CancellationToken,
) -> Result<JsonValue, String> {
call_nested_tool(
self.exec.clone(),
self.tool_runtime.clone(),
invocation,
cancellation_token,
)
.await
.map_err(|error| error.to_string())
}
async fn notify(&self, call_id: String, cell_id: CellId, text: String) -> Result<(), String> {
if text.trim().is_empty() {
return Ok(());
}
self.exec
.session
.inject_if_running(vec![ResponseItem::CustomToolCallOutput {
call_id,
name: Some(PUBLIC_TOOL_NAME.to_string()),
output: FunctionCallOutputPayload::from_text(text),
}])
.await
.map_err(|_| {
format!("failed to inject exec notify message for cell {cell_id}: no active turn")
})
}
}
@@ -38,9 +38,22 @@ impl CodeModeExecuteHandler {
let exec = ExecContext { session, turn }; let exec = ExecContext { session, turn };
let enabled_tools = let enabled_tools =
codex_tools::collect_code_mode_tool_definitions(&self.nested_tool_specs); codex_tools::collect_code_mode_tool_definitions(&self.nested_tool_specs);
// Allocate before starting V8 so the trace can create the parent let started_at = std::time::Instant::now();
// CodeCell before model-authored JavaScript issues nested tool calls. let started_cell = exec
let runtime_cell_id = exec.session.services.code_mode_service.allocate_cell_id(); .session
.services
.code_mode_service
.execute(codex_code_mode::ExecuteRequest {
tool_call_id: call_id.clone(),
enabled_tools,
source: args.code.clone(),
yield_time_ms: args.yield_time_ms,
max_output_tokens: args.max_output_tokens,
})
.await
.map_err(FunctionCallError::RespondToModel)?;
let cell_id = started_cell.cell_id.clone();
let runtime_cell_id = cell_id.to_string();
let code_cell_trace = exec let code_cell_trace = exec
.session .session
.services .services
@@ -51,19 +64,12 @@ impl CodeModeExecuteHandler {
call_id.as_str(), call_id.as_str(),
args.code.as_str(), args.code.as_str(),
); );
let started_at = std::time::Instant::now(); exec.session
let response = exec
.session
.services .services
.code_mode_service .code_mode_service
.execute(codex_code_mode::ExecuteRequest { .mark_cell_ready_for_dispatch(&cell_id);
cell_id: runtime_cell_id, let response = started_cell
tool_call_id: call_id, .initial_response()
enabled_tools,
source: args.code,
yield_time_ms: args.yield_time_ms,
max_output_tokens: args.max_output_tokens,
})
.await .await
.map_err(FunctionCallError::RespondToModel)?; .map_err(FunctionCallError::RespondToModel)?;
// Record the raw runtime boundary. The model-visible custom-tool output // Record the raw runtime boundary. The model-visible custom-tool output
@@ -74,6 +80,10 @@ impl CodeModeExecuteHandler {
// here when the first response also ended the runtime. // here when the first response also ended the runtime.
if !matches!(response, codex_code_mode::RuntimeResponse::Yielded { .. }) { if !matches!(response, codex_code_mode::RuntimeResponse::Yielded { .. }) {
code_cell_trace.record_ended(&response); code_cell_trace.record_ended(&response);
exec.session
.services
.code_mode_service
.finish_cell_dispatch(&cell_id);
} }
handle_runtime_response(&exec, response, args.max_output_tokens, started_at) handle_runtime_response(&exec, response, args.max_output_tokens, started_at)
.await .await
+50 -57
View File
@@ -1,3 +1,4 @@
mod delegate;
mod execute_handler; mod execute_handler;
pub(crate) mod execute_spec; pub(crate) mod execute_spec;
mod response_adapter; mod response_adapter;
@@ -7,13 +8,12 @@ pub(crate) mod wait_spec;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use codex_code_mode::CellId;
use codex_code_mode::CodeModeNestedToolCall; use codex_code_mode::CodeModeNestedToolCall;
use codex_code_mode::CodeModeSession;
use codex_code_mode::CodeModeToolKind; use codex_code_mode::CodeModeToolKind;
use codex_code_mode::CodeModeTurnHost;
use codex_code_mode::RuntimeResponse; use codex_code_mode::RuntimeResponse;
use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseItem;
use serde_json::Value as JsonValue; use serde_json::Value as JsonValue;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
@@ -36,6 +36,8 @@ use codex_utils_output_truncation::TruncationPolicy;
use codex_utils_output_truncation::formatted_truncate_text_content_items_with_policy; use codex_utils_output_truncation::formatted_truncate_text_content_items_with_policy;
use codex_utils_output_truncation::truncate_function_output_items_with_policy; use codex_utils_output_truncation::truncate_function_output_items_with_policy;
use delegate::CodeModeDispatchBroker;
use delegate::CodeModeDispatchWorker;
pub(crate) use execute_handler::CodeModeExecuteHandler; pub(crate) use execute_handler::CodeModeExecuteHandler;
use response_adapter::into_function_call_output_content_items; use response_adapter::into_function_call_output_content_items;
pub(crate) use wait_handler::CodeModeWaitHandler; pub(crate) use wait_handler::CodeModeWaitHandler;
@@ -56,42 +58,67 @@ pub(crate) struct ExecContext {
} }
pub(crate) struct CodeModeService { pub(crate) struct CodeModeService {
inner: codex_code_mode::CodeModeService, session: Option<Arc<dyn CodeModeSession>>,
dispatch_broker: Arc<CodeModeDispatchBroker>,
} }
impl CodeModeService { impl CodeModeService {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
let dispatch_broker = Arc::new(CodeModeDispatchBroker::new());
Self { Self {
inner: codex_code_mode::CodeModeService::new(), session: Some(Arc::new(codex_code_mode::CodeModeService::with_delegate(
dispatch_broker.clone(),
))),
dispatch_broker,
} }
} }
pub(crate) fn allocate_cell_id(&self) -> String {
self.inner.allocate_cell_id()
}
pub(crate) async fn execute( pub(crate) async fn execute(
&self, &self,
request: codex_code_mode::ExecuteRequest, request: codex_code_mode::ExecuteRequest,
) -> Result<RuntimeResponse, String> { ) -> Result<codex_code_mode::StartedCell, String> {
self.inner.execute(request).await self.session()?.execute(request).await
} }
pub(crate) async fn wait( pub(crate) async fn wait(
&self, &self,
request: codex_code_mode::WaitRequest, request: codex_code_mode::WaitRequest,
) -> Result<codex_code_mode::WaitOutcome, String> { ) -> Result<codex_code_mode::WaitOutcome, String> {
self.inner.wait(request).await self.session()?.wait(request).await
} }
pub(crate) async fn start_turn_worker( pub(crate) async fn terminate(
&self,
cell_id: CellId,
) -> Result<codex_code_mode::WaitOutcome, String> {
self.session()?.terminate(cell_id).await
}
pub(crate) async fn shutdown(&self) -> Result<(), String> {
match &self.session {
Some(session) => session.shutdown().await,
None => Ok(()),
}
}
pub(crate) fn mark_cell_ready_for_dispatch(&self, cell_id: &codex_code_mode::CellId) {
self.dispatch_broker.mark_cell_ready_for_dispatch(cell_id);
}
pub(crate) fn finish_cell_dispatch(&self, cell_id: &CellId) {
self.dispatch_broker.close_cell(cell_id);
}
pub(crate) fn start_turn_worker(
&self, &self,
session: &Arc<Session>, session: &Arc<Session>,
turn: &Arc<TurnContext>, turn: &Arc<TurnContext>,
router: Arc<ToolRouter>, router: Arc<ToolRouter>,
tracker: SharedTurnDiffTracker, tracker: SharedTurnDiffTracker,
) -> Option<codex_code_mode::CodeModeTurnWorker> { ) -> Option<CodeModeDispatchWorker> {
if !matches!(turn.tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly) { if !matches!(turn.tool_mode, ToolMode::CodeMode | ToolMode::CodeModeOnly)
|| self.session.is_none()
{
return None; return None;
} }
@@ -99,50 +126,16 @@ impl CodeModeService {
session: Arc::clone(session), session: Arc::clone(session),
turn: Arc::clone(turn), turn: Arc::clone(turn),
}; };
let tool_runtime = Some(
ToolCallRuntime::new(router, Arc::clone(session), Arc::clone(turn), tracker); self.dispatch_broker
let host = Arc::new(CoreTurnHost { exec, tool_runtime }); .start_turn_worker(exec, router, tracker),
Some(self.inner.start_turn_worker(host))
}
}
struct CoreTurnHost {
exec: ExecContext,
tool_runtime: ToolCallRuntime,
}
#[async_trait::async_trait]
impl CodeModeTurnHost for CoreTurnHost {
async fn invoke_tool(
&self,
invocation: CodeModeNestedToolCall,
cancellation_token: CancellationToken,
) -> Result<JsonValue, String> {
call_nested_tool(
self.exec.clone(),
self.tool_runtime.clone(),
invocation,
cancellation_token,
) )
.await
.map_err(|error| error.to_string())
} }
async fn notify(&self, call_id: String, cell_id: String, text: String) -> Result<(), String> { fn session(&self) -> Result<&Arc<dyn CodeModeSession>, String> {
if text.trim().is_empty() { self.session
return Ok(()); .as_ref()
} .ok_or_else(|| "code mode is unavailable".to_string())
self.exec
.session
.inject_if_running(vec![ResponseItem::CustomToolCallOutput {
call_id,
name: Some(PUBLIC_TOOL_NAME.to_string()),
output: FunctionCallOutputPayload::from_text(text),
}])
.await
.map_err(|_| {
format!("failed to inject exec notify message for cell {cell_id}: no active turn")
})
} }
} }
@@ -273,7 +266,7 @@ async fn call_nested_tool(
.handle_tool_call_with_source( .handle_tool_call_with_source(
call, call,
ToolCallSource::CodeMode { ToolCallSource::CodeMode {
cell_id, cell_id: cell_id.to_string(),
runtime_tool_call_id, runtime_tool_call_id,
}, },
cancellation_token, cancellation_token,
@@ -73,16 +73,23 @@ impl ToolExecutor<ToolInvocation> for CodeModeWaitHandler {
let args: ExecWaitArgs = parse_arguments(&arguments)?; let args: ExecWaitArgs = parse_arguments(&arguments)?;
let exec = ExecContext { session, turn }; let exec = ExecContext { session, turn };
let started_at = std::time::Instant::now(); let started_at = std::time::Instant::now();
let wait_response = exec let cell_id = codex_code_mode::CellId::new(args.cell_id);
.session let wait_response = if args.terminate {
exec.session
.services
.code_mode_service
.terminate(cell_id)
.await
} else {
exec.session
.services .services
.code_mode_service .code_mode_service
.wait(codex_code_mode::WaitRequest { .wait(codex_code_mode::WaitRequest {
cell_id: args.cell_id, cell_id,
yield_time_ms: args.yield_time_ms, yield_time_ms: args.yield_time_ms,
terminate: args.terminate,
}) })
.await .await
}
.map_err(FunctionCallError::RespondToModel)?; .map_err(FunctionCallError::RespondToModel)?;
if let codex_code_mode::WaitOutcome::LiveCell(response) = &wait_response if let codex_code_mode::WaitOutcome::LiveCell(response) = &wait_response
&& !matches!(response, codex_code_mode::RuntimeResponse::Yielded { .. }) && !matches!(response, codex_code_mode::RuntimeResponse::Yielded { .. })
@@ -98,8 +105,15 @@ impl ToolExecutor<ToolInvocation> for CodeModeWaitHandler {
exec.session exec.session
.services .services
.rollout_thread_trace .rollout_thread_trace
.code_cell_trace_context(exec.turn.sub_id.as_str(), runtime_cell_id) .code_cell_trace_context(
exec.turn.sub_id.as_str(),
runtime_cell_id.as_str(),
)
.record_ended(response); .record_ended(response);
exec.session
.services
.code_mode_service
.finish_cell_dispatch(runtime_cell_id);
} }
handle_runtime_response(&exec, wait_response.into(), args.max_tokens, started_at) handle_runtime_response(&exec, wait_response.into(), args.max_tokens, started_at)
.await .await