mirror of
https://github.com/openai/codex.git
synced 2026-05-15 08:42:34 +00:00
Moves Code Mode to a new crate with no dependencies on codex. This
create encodes the code mode semantics that we want for lifetime,
mounting, tool calling.
The model-facing surface is mostly unchanged. `exec` still runs raw
JavaScript, `wait` still resumes or terminates a `cell_id`, nested tools
are still available through `tools.*`, and helpers like `text`, `image`,
`store`, `load`, `notify`, `yield_control`, and `exit` still exist.
The major change is underneath that surface:
- Old code mode was an external Node runtime.
- New code mode is an in-process V8 runtime embedded directly in Rust.
- Old code mode managed cells inside a long-lived Node runner process.
- New code mode manages cells in Rust, with one V8 runtime thread per
active `exec`.
- Old code mode used JSON protocol messages over child stdin/stdout plus
Node worker-thread messages.
- New code mode uses Rust channels and direct V8 callbacks/events.
This PR also fixes the two migration regressions that fell out of that
substrate change:
- `wait { terminate: true }` now waits for the V8 runtime to actually
stop before reporting termination.
- synchronous top-level `exit()` now succeeds again instead of surfacing
as a script error.
---
- `core/src/tools/code_mode/*` is now mostly an adapter layer for the
public `exec` / `wait` tools.
- `code-mode/src/service.rs` owns cell sessions and async control flow
in Rust.
- `code-mode/src/runtime/*.rs` owns the embedded V8 isolate and
JavaScript execution.
- each `exec` spawns a dedicated runtime thread plus a Rust
session-control task.
- helper globals are installed directly into the V8 context instead of
being injected through a source prelude.
- helper modules like `tools.js` and `@openai/code_mode` are synthesized
through V8 module resolution callbacks in Rust.
---
Also added a benchmark for showing the speed of init and use of a code
mode env:
```
$ cargo bench -p codex-code-mode --bench exec_overhead -- --samples 30 --warm-iterations 25 --tool-counts 0,32,128
Finished [`bench` profile [optimized]](https://doc.rust-lang.org/cargo/reference/profiles.html#default-profiles) target(s) in 0.18s
Running benches/exec_overhead.rs (target/release/deps/exec_overhead-008c440d800545ae)
exec_overhead: samples=30, warm_iterations=25, tool_counts=[0, 32, 128]
scenario tools samples warmups iters mean/exec p95/exec rssΔ p50 rssΔ max
cold_exec 0 30 0 1 1.13ms 1.20ms 8.05MiB 8.06MiB
warm_exec 0 30 1 25 473.43us 512.49us 912.00KiB 1.33MiB
cold_exec 32 30 0 1 1.03ms 1.15ms 8.08MiB 8.11MiB
warm_exec 32 30 1 25 509.73us 545.76us 960.00KiB 1.30MiB
cold_exec 128 30 0 1 1.14ms 1.19ms 8.30MiB 8.34MiB
warm_exec 128 30 1 25 575.08us 591.03us 736.00KiB 864.00KiB
memory uses a fresh-process max RSS delta for each scenario
```
---------
Co-authored-by: Codex <noreply@openai.com>
350 lines
9.8 KiB
Rust
350 lines
9.8 KiB
Rust
mod callbacks;
|
|
mod globals;
|
|
mod module_loader;
|
|
mod value;
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::OnceLock;
|
|
use std::sync::mpsc as std_mpsc;
|
|
use std::thread;
|
|
|
|
use serde_json::Value as JsonValue;
|
|
use tokio::sync::mpsc;
|
|
|
|
use crate::description::EnabledToolMetadata;
|
|
use crate::description::ToolDefinition;
|
|
use crate::description::enabled_tool_metadata;
|
|
use crate::response::FunctionCallOutputContentItem;
|
|
|
|
pub const DEFAULT_EXEC_YIELD_TIME_MS: u64 = 10_000;
|
|
pub const DEFAULT_WAIT_YIELD_TIME_MS: u64 = 10_000;
|
|
pub const DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL: usize = 10_000;
|
|
const EXIT_SENTINEL: &str = "__codex_code_mode_exit__";
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct ExecuteRequest {
|
|
pub tool_call_id: String,
|
|
pub enabled_tools: Vec<ToolDefinition>,
|
|
pub source: String,
|
|
pub stored_values: HashMap<String, JsonValue>,
|
|
pub yield_time_ms: Option<u64>,
|
|
pub max_output_tokens: Option<usize>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct WaitRequest {
|
|
pub cell_id: String,
|
|
pub yield_time_ms: u64,
|
|
pub terminate: bool,
|
|
}
|
|
|
|
#[derive(Debug, PartialEq)]
|
|
pub enum RuntimeResponse {
|
|
Yielded {
|
|
cell_id: String,
|
|
content_items: Vec<FunctionCallOutputContentItem>,
|
|
},
|
|
Terminated {
|
|
cell_id: String,
|
|
content_items: Vec<FunctionCallOutputContentItem>,
|
|
},
|
|
Result {
|
|
cell_id: String,
|
|
content_items: Vec<FunctionCallOutputContentItem>,
|
|
stored_values: HashMap<String, JsonValue>,
|
|
error_text: Option<String>,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) enum TurnMessage {
|
|
ToolCall {
|
|
cell_id: String,
|
|
id: String,
|
|
name: String,
|
|
input: Option<JsonValue>,
|
|
},
|
|
Notify {
|
|
cell_id: String,
|
|
call_id: String,
|
|
text: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) enum RuntimeCommand {
|
|
ToolResponse { id: String, result: JsonValue },
|
|
ToolError { id: String, error_text: String },
|
|
Terminate,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub(crate) enum RuntimeEvent {
|
|
Started,
|
|
ContentItem(FunctionCallOutputContentItem),
|
|
YieldRequested,
|
|
ToolCall {
|
|
id: String,
|
|
name: String,
|
|
input: Option<JsonValue>,
|
|
},
|
|
Notify {
|
|
call_id: String,
|
|
text: String,
|
|
},
|
|
Result {
|
|
stored_values: HashMap<String, JsonValue>,
|
|
error_text: Option<String>,
|
|
},
|
|
}
|
|
|
|
pub(crate) fn spawn_runtime(
|
|
request: ExecuteRequest,
|
|
event_tx: mpsc::UnboundedSender<RuntimeEvent>,
|
|
) -> Result<(std_mpsc::Sender<RuntimeCommand>, v8::IsolateHandle), String> {
|
|
let (command_tx, command_rx) = std_mpsc::channel();
|
|
let (isolate_handle_tx, isolate_handle_rx) = std_mpsc::sync_channel(1);
|
|
let enabled_tools = request
|
|
.enabled_tools
|
|
.iter()
|
|
.map(enabled_tool_metadata)
|
|
.collect::<Vec<_>>();
|
|
let config = RuntimeConfig {
|
|
tool_call_id: request.tool_call_id,
|
|
enabled_tools,
|
|
source: request.source,
|
|
stored_values: request.stored_values,
|
|
};
|
|
|
|
thread::spawn(move || {
|
|
run_runtime(config, event_tx, command_rx, isolate_handle_tx);
|
|
});
|
|
|
|
let isolate_handle = isolate_handle_rx
|
|
.recv()
|
|
.map_err(|_| "failed to initialize code mode runtime".to_string())?;
|
|
Ok((command_tx, isolate_handle))
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct RuntimeConfig {
|
|
tool_call_id: String,
|
|
enabled_tools: Vec<EnabledToolMetadata>,
|
|
source: String,
|
|
stored_values: HashMap<String, JsonValue>,
|
|
}
|
|
|
|
pub(super) struct RuntimeState {
|
|
event_tx: mpsc::UnboundedSender<RuntimeEvent>,
|
|
pending_tool_calls: HashMap<String, v8::Global<v8::PromiseResolver>>,
|
|
stored_values: HashMap<String, JsonValue>,
|
|
enabled_tools: Vec<EnabledToolMetadata>,
|
|
next_tool_call_id: u64,
|
|
tool_call_id: String,
|
|
exit_requested: bool,
|
|
}
|
|
|
|
pub(super) enum CompletionState {
|
|
Pending,
|
|
Completed {
|
|
stored_values: HashMap<String, JsonValue>,
|
|
error_text: Option<String>,
|
|
},
|
|
}
|
|
|
|
fn initialize_v8() {
|
|
static PLATFORM: OnceLock<v8::SharedRef<v8::Platform>> = OnceLock::new();
|
|
|
|
let _ = PLATFORM.get_or_init(|| {
|
|
let platform = v8::new_default_platform(0, false).make_shared();
|
|
v8::V8::initialize_platform(platform.clone());
|
|
v8::V8::initialize();
|
|
platform
|
|
});
|
|
}
|
|
|
|
fn run_runtime(
|
|
config: RuntimeConfig,
|
|
event_tx: mpsc::UnboundedSender<RuntimeEvent>,
|
|
command_rx: std_mpsc::Receiver<RuntimeCommand>,
|
|
isolate_handle_tx: std_mpsc::SyncSender<v8::IsolateHandle>,
|
|
) {
|
|
initialize_v8();
|
|
|
|
let isolate = &mut v8::Isolate::new(v8::CreateParams::default());
|
|
let isolate_handle = isolate.thread_safe_handle();
|
|
if isolate_handle_tx.send(isolate_handle).is_err() {
|
|
return;
|
|
}
|
|
isolate.set_host_import_module_dynamically_callback(module_loader::dynamic_import_callback);
|
|
|
|
v8::scope!(let scope, isolate);
|
|
let context = v8::Context::new(scope, Default::default());
|
|
let scope = &mut v8::ContextScope::new(scope, context);
|
|
|
|
scope.set_slot(RuntimeState {
|
|
event_tx: event_tx.clone(),
|
|
pending_tool_calls: HashMap::new(),
|
|
stored_values: config.stored_values,
|
|
enabled_tools: config.enabled_tools,
|
|
next_tool_call_id: 1,
|
|
tool_call_id: config.tool_call_id,
|
|
exit_requested: false,
|
|
});
|
|
|
|
if let Err(error_text) = globals::install_globals(scope) {
|
|
send_result(&event_tx, HashMap::new(), Some(error_text));
|
|
return;
|
|
}
|
|
|
|
let _ = event_tx.send(RuntimeEvent::Started);
|
|
|
|
let pending_promise = match module_loader::evaluate_main_module(scope, &config.source) {
|
|
Ok(pending_promise) => pending_promise,
|
|
Err(error_text) => {
|
|
capture_scope_send_error(scope, &event_tx, Some(error_text));
|
|
return;
|
|
}
|
|
};
|
|
|
|
match module_loader::completion_state(scope, pending_promise.as_ref()) {
|
|
CompletionState::Completed {
|
|
stored_values,
|
|
error_text,
|
|
} => {
|
|
send_result(&event_tx, stored_values, error_text);
|
|
return;
|
|
}
|
|
CompletionState::Pending => {}
|
|
}
|
|
|
|
let mut pending_promise = pending_promise;
|
|
loop {
|
|
let Ok(command) = command_rx.recv() else {
|
|
break;
|
|
};
|
|
match command {
|
|
RuntimeCommand::Terminate => break,
|
|
RuntimeCommand::ToolResponse { id, result } => {
|
|
if let Err(error_text) =
|
|
module_loader::resolve_tool_response(scope, &id, Ok(result))
|
|
{
|
|
capture_scope_send_error(scope, &event_tx, Some(error_text));
|
|
return;
|
|
}
|
|
}
|
|
RuntimeCommand::ToolError { id, error_text } => {
|
|
if let Err(runtime_error) =
|
|
module_loader::resolve_tool_response(scope, &id, Err(error_text))
|
|
{
|
|
capture_scope_send_error(scope, &event_tx, Some(runtime_error));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
scope.perform_microtask_checkpoint();
|
|
match module_loader::completion_state(scope, pending_promise.as_ref()) {
|
|
CompletionState::Completed {
|
|
stored_values,
|
|
error_text,
|
|
} => {
|
|
send_result(&event_tx, stored_values, error_text);
|
|
return;
|
|
}
|
|
CompletionState::Pending => {}
|
|
}
|
|
|
|
if let Some(promise) = pending_promise.as_ref() {
|
|
let promise = v8::Local::new(scope, promise);
|
|
if promise.state() != v8::PromiseState::Pending {
|
|
pending_promise = None;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn capture_scope_send_error(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
event_tx: &mpsc::UnboundedSender<RuntimeEvent>,
|
|
error_text: Option<String>,
|
|
) {
|
|
let stored_values = scope
|
|
.get_slot::<RuntimeState>()
|
|
.map(|state| state.stored_values.clone())
|
|
.unwrap_or_default();
|
|
|
|
send_result(event_tx, stored_values, error_text);
|
|
}
|
|
|
|
fn send_result(
|
|
event_tx: &mpsc::UnboundedSender<RuntimeEvent>,
|
|
stored_values: HashMap<String, JsonValue>,
|
|
error_text: Option<String>,
|
|
) {
|
|
let _ = event_tx.send(RuntimeEvent::Result {
|
|
stored_values,
|
|
error_text,
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::collections::HashMap;
|
|
use std::time::Duration;
|
|
|
|
use pretty_assertions::assert_eq;
|
|
use tokio::sync::mpsc;
|
|
|
|
use super::ExecuteRequest;
|
|
use super::RuntimeEvent;
|
|
use super::spawn_runtime;
|
|
|
|
fn execute_request(source: &str) -> ExecuteRequest {
|
|
ExecuteRequest {
|
|
tool_call_id: "call_1".to_string(),
|
|
enabled_tools: Vec::new(),
|
|
source: source.to_string(),
|
|
stored_values: HashMap::new(),
|
|
yield_time_ms: Some(1),
|
|
max_output_tokens: None,
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn terminate_execution_stops_cpu_bound_module() {
|
|
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
|
|
let (_runtime_tx, runtime_terminate_handle) =
|
|
spawn_runtime(execute_request("while (true) {}"), event_tx).unwrap();
|
|
|
|
let started_event = tokio::time::timeout(Duration::from_secs(1), event_rx.recv())
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
assert!(matches!(started_event, RuntimeEvent::Started));
|
|
|
|
assert!(runtime_terminate_handle.terminate_execution());
|
|
|
|
let result_event = tokio::time::timeout(Duration::from_secs(1), event_rx.recv())
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
let RuntimeEvent::Result {
|
|
stored_values,
|
|
error_text,
|
|
} = result_event
|
|
else {
|
|
panic!("expected runtime result after termination");
|
|
};
|
|
assert_eq!(stored_values, HashMap::new());
|
|
assert!(error_text.is_some());
|
|
|
|
assert!(
|
|
tokio::time::timeout(Duration::from_secs(1), event_rx.recv())
|
|
.await
|
|
.unwrap()
|
|
.is_none()
|
|
);
|
|
}
|
|
}
|