mirror of
https://github.com/openai/codex.git
synced 2026-05-18 10:12:59 +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>
236 lines
7.9 KiB
Rust
236 lines
7.9 KiB
Rust
use serde_json::Value as JsonValue;
|
|
|
|
use super::CompletionState;
|
|
use super::EXIT_SENTINEL;
|
|
use super::RuntimeState;
|
|
use super::value::json_to_v8;
|
|
use super::value::value_to_error_text;
|
|
|
|
pub(super) fn evaluate_main_module(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
source_text: &str,
|
|
) -> Result<Option<v8::Global<v8::Promise>>, String> {
|
|
let tc = std::pin::pin!(v8::TryCatch::new(scope));
|
|
let mut tc = tc.init();
|
|
let source = v8::String::new(&tc, source_text)
|
|
.ok_or_else(|| "failed to allocate exec source".to_string())?;
|
|
let origin = script_origin(&mut tc, "exec_main.mjs")?;
|
|
let mut source = v8::script_compiler::Source::new(source, Some(&origin));
|
|
let module = v8::script_compiler::compile_module(&tc, &mut source).ok_or_else(|| {
|
|
tc.exception()
|
|
.map(|exception| value_to_error_text(&mut tc, exception))
|
|
.unwrap_or_else(|| "unknown code mode exception".to_string())
|
|
})?;
|
|
module
|
|
.instantiate_module(&tc, resolve_module_callback)
|
|
.ok_or_else(|| {
|
|
tc.exception()
|
|
.map(|exception| value_to_error_text(&mut tc, exception))
|
|
.unwrap_or_else(|| "unknown code mode exception".to_string())
|
|
})?;
|
|
let result = match module.evaluate(&tc) {
|
|
Some(result) => result,
|
|
None => {
|
|
if let Some(exception) = tc.exception() {
|
|
if is_exit_exception(&mut tc, exception) {
|
|
return Ok(None);
|
|
}
|
|
return Err(value_to_error_text(&mut tc, exception));
|
|
}
|
|
return Err("unknown code mode exception".to_string());
|
|
}
|
|
};
|
|
tc.perform_microtask_checkpoint();
|
|
|
|
if result.is_promise() {
|
|
let promise = v8::Local::<v8::Promise>::try_from(result)
|
|
.map_err(|_| "failed to read exec promise".to_string())?;
|
|
return Ok(Some(v8::Global::new(&tc, promise)));
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
|
|
fn is_exit_exception(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
exception: v8::Local<'_, v8::Value>,
|
|
) -> bool {
|
|
scope
|
|
.get_slot::<RuntimeState>()
|
|
.map(|state| state.exit_requested)
|
|
.unwrap_or(false)
|
|
&& exception.is_string()
|
|
&& exception.to_rust_string_lossy(scope) == EXIT_SENTINEL
|
|
}
|
|
|
|
pub(super) fn resolve_tool_response(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
id: &str,
|
|
response: Result<JsonValue, String>,
|
|
) -> Result<(), String> {
|
|
let resolver = {
|
|
let state = scope
|
|
.get_slot_mut::<RuntimeState>()
|
|
.ok_or_else(|| "runtime state unavailable".to_string())?;
|
|
state.pending_tool_calls.remove(id)
|
|
}
|
|
.ok_or_else(|| format!("unknown tool call `{id}`"))?;
|
|
|
|
let tc = std::pin::pin!(v8::TryCatch::new(scope));
|
|
let mut tc = tc.init();
|
|
let resolver = v8::Local::new(&tc, &resolver);
|
|
match response {
|
|
Ok(result) => {
|
|
let value = json_to_v8(&mut tc, &result)
|
|
.ok_or_else(|| "failed to serialize tool response".to_string())?;
|
|
resolver.resolve(&tc, value);
|
|
}
|
|
Err(error_text) => {
|
|
let value = v8::String::new(&tc, &error_text)
|
|
.ok_or_else(|| "failed to allocate tool error".to_string())?;
|
|
resolver.reject(&tc, value.into());
|
|
}
|
|
}
|
|
if tc.has_caught() {
|
|
return Err(tc
|
|
.exception()
|
|
.map(|exception| value_to_error_text(&mut tc, exception))
|
|
.unwrap_or_else(|| "unknown code mode exception".to_string()));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn completion_state(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
pending_promise: Option<&v8::Global<v8::Promise>>,
|
|
) -> CompletionState {
|
|
let stored_values = scope
|
|
.get_slot::<RuntimeState>()
|
|
.map(|state| state.stored_values.clone())
|
|
.unwrap_or_default();
|
|
|
|
let Some(pending_promise) = pending_promise else {
|
|
return CompletionState::Completed {
|
|
stored_values,
|
|
error_text: None,
|
|
};
|
|
};
|
|
|
|
let promise = v8::Local::new(scope, pending_promise);
|
|
match promise.state() {
|
|
v8::PromiseState::Pending => CompletionState::Pending,
|
|
v8::PromiseState::Fulfilled => CompletionState::Completed {
|
|
stored_values,
|
|
error_text: None,
|
|
},
|
|
v8::PromiseState::Rejected => {
|
|
let result = promise.result(scope);
|
|
let error_text = if is_exit_exception(scope, result) {
|
|
None
|
|
} else {
|
|
Some(value_to_error_text(scope, result))
|
|
};
|
|
CompletionState::Completed {
|
|
stored_values,
|
|
error_text,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn script_origin<'s>(
|
|
scope: &mut v8::PinScope<'s, '_>,
|
|
resource_name_: &str,
|
|
) -> Result<v8::ScriptOrigin<'s>, String> {
|
|
let resource_name = v8::String::new(scope, resource_name_)
|
|
.ok_or_else(|| "failed to allocate script origin".to_string())?;
|
|
let source_map_url = v8::String::new(scope, resource_name_)
|
|
.ok_or_else(|| "failed to allocate source map url".to_string())?;
|
|
Ok(v8::ScriptOrigin::new(
|
|
scope,
|
|
resource_name.into(),
|
|
0,
|
|
0,
|
|
true,
|
|
0,
|
|
Some(source_map_url.into()),
|
|
true,
|
|
false,
|
|
true,
|
|
None,
|
|
))
|
|
}
|
|
|
|
fn resolve_module_callback<'s>(
|
|
context: v8::Local<'s, v8::Context>,
|
|
specifier: v8::Local<'s, v8::String>,
|
|
_import_attributes: v8::Local<'s, v8::FixedArray>,
|
|
_referrer: v8::Local<'s, v8::Module>,
|
|
) -> Option<v8::Local<'s, v8::Module>> {
|
|
v8::callback_scope!(unsafe scope, context);
|
|
let specifier = specifier.to_rust_string_lossy(scope);
|
|
resolve_module(scope, &specifier)
|
|
}
|
|
|
|
pub(super) fn dynamic_import_callback<'s>(
|
|
scope: &mut v8::PinScope<'s, '_>,
|
|
_host_defined_options: v8::Local<'s, v8::Data>,
|
|
_resource_name: v8::Local<'s, v8::Value>,
|
|
specifier: v8::Local<'s, v8::String>,
|
|
_import_attributes: v8::Local<'s, v8::FixedArray>,
|
|
) -> Option<v8::Local<'s, v8::Promise>> {
|
|
let specifier = specifier.to_rust_string_lossy(scope);
|
|
let resolver = v8::PromiseResolver::new(scope)?;
|
|
|
|
match resolve_module(scope, &specifier) {
|
|
Some(module) => {
|
|
if module.get_status() == v8::ModuleStatus::Uninstantiated
|
|
&& module
|
|
.instantiate_module(scope, resolve_module_callback)
|
|
.is_none()
|
|
{
|
|
let error = v8::String::new(scope, "failed to instantiate module")
|
|
.map(Into::into)
|
|
.unwrap_or_else(|| v8::undefined(scope).into());
|
|
resolver.reject(scope, error);
|
|
return Some(resolver.get_promise(scope));
|
|
}
|
|
if matches!(
|
|
module.get_status(),
|
|
v8::ModuleStatus::Instantiated | v8::ModuleStatus::Evaluated
|
|
) && module.evaluate(scope).is_none()
|
|
{
|
|
let error = v8::String::new(scope, "failed to evaluate module")
|
|
.map(Into::into)
|
|
.unwrap_or_else(|| v8::undefined(scope).into());
|
|
resolver.reject(scope, error);
|
|
return Some(resolver.get_promise(scope));
|
|
}
|
|
let namespace = module.get_module_namespace();
|
|
resolver.resolve(scope, namespace);
|
|
Some(resolver.get_promise(scope))
|
|
}
|
|
None => {
|
|
let error = v8::String::new(scope, "unsupported import in exec")
|
|
.map(Into::into)
|
|
.unwrap_or_else(|| v8::undefined(scope).into());
|
|
resolver.reject(scope, error);
|
|
Some(resolver.get_promise(scope))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn resolve_module<'s>(
|
|
scope: &mut v8::PinScope<'s, '_>,
|
|
specifier: &str,
|
|
) -> Option<v8::Local<'s, v8::Module>> {
|
|
if let Some(message) =
|
|
v8::String::new(scope, &format!("Unsupported import in exec: {specifier}"))
|
|
{
|
|
scope.throw_exception(message.into());
|
|
} else {
|
|
scope.throw_exception(v8::undefined(scope).into());
|
|
}
|
|
None
|
|
}
|