mirror of
https://github.com/openai/codex.git
synced 2026-11-13 14:46:14 +00:00
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:
1 parent
451b386442
commit
c9dc0f6338
11 files changed
+1056
-458
No files matched your search
Generated
-2
@@ -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",
|
||||||
|
|||||||
@@ -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"] }
|
||||||
|
|||||||
@@ -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";
|
||||||
@@ -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(),
|
||||||
|
|||||||
+617
-335
File diff suppressed because it is too large.
Load diff
@@ -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()
|
||||||
|
|||||||
@@ -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
|
&sess,
|
||||||
.code_mode_service
|
&turn_context,
|
||||||
.start_turn_worker(
|
Arc::clone(&router),
|
||||||
&sess,
|
Arc::clone(&turn_diff_tracker),
|
||||||
&turn_context,
|
);
|
||||||
Arc::clone(&router),
|
|
||||||
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
|
||||||
|
|||||||
@@ -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,17 +73,24 @@ 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 {
|
||||||
.services
|
exec.session
|
||||||
.code_mode_service
|
.services
|
||||||
.wait(codex_code_mode::WaitRequest {
|
.code_mode_service
|
||||||
cell_id: args.cell_id,
|
.terminate(cell_id)
|
||||||
yield_time_ms: args.yield_time_ms,
|
.await
|
||||||
terminate: args.terminate,
|
} else {
|
||||||
})
|
exec.session
|
||||||
.await
|
.services
|
||||||
.map_err(FunctionCallError::RespondToModel)?;
|
.code_mode_service
|
||||||
|
.wait(codex_code_mode::WaitRequest {
|
||||||
|
cell_id,
|
||||||
|
yield_time_ms: args.yield_time_ms,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
.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
|
||||||
|
|||||||
Reference in new issue
Block a user