mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
e4eedd6170
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>
210 lines
6.1 KiB
Rust
210 lines
6.1 KiB
Rust
use crate::response::FunctionCallOutputContentItem;
|
|
|
|
use super::EXIT_SENTINEL;
|
|
use super::RuntimeEvent;
|
|
use super::RuntimeState;
|
|
use super::value::json_to_v8;
|
|
use super::value::normalize_output_image;
|
|
use super::value::serialize_output_text;
|
|
use super::value::throw_type_error;
|
|
use super::value::v8_value_to_json;
|
|
|
|
pub(super) fn tool_callback(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
args: v8::FunctionCallbackArguments,
|
|
mut retval: v8::ReturnValue<v8::Value>,
|
|
) {
|
|
let tool_name = args.data().to_rust_string_lossy(scope);
|
|
let input = if args.length() == 0 {
|
|
Ok(None)
|
|
} else {
|
|
v8_value_to_json(scope, args.get(0))
|
|
};
|
|
let input = match input {
|
|
Ok(input) => input,
|
|
Err(error_text) => {
|
|
throw_type_error(scope, &error_text);
|
|
return;
|
|
}
|
|
};
|
|
|
|
let Some(resolver) = v8::PromiseResolver::new(scope) else {
|
|
throw_type_error(scope, "failed to create tool promise");
|
|
return;
|
|
};
|
|
let promise = resolver.get_promise(scope);
|
|
|
|
let resolver = v8::Global::new(scope, resolver);
|
|
let Some(state) = scope.get_slot_mut::<RuntimeState>() else {
|
|
throw_type_error(scope, "runtime state unavailable");
|
|
return;
|
|
};
|
|
let id = format!("tool-{}", state.next_tool_call_id);
|
|
state.next_tool_call_id = state.next_tool_call_id.saturating_add(1);
|
|
let event_tx = state.event_tx.clone();
|
|
state.pending_tool_calls.insert(id.clone(), resolver);
|
|
let _ = event_tx.send(RuntimeEvent::ToolCall {
|
|
id,
|
|
name: tool_name,
|
|
input,
|
|
});
|
|
retval.set(promise.into());
|
|
}
|
|
|
|
pub(super) fn text_callback(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
args: v8::FunctionCallbackArguments,
|
|
mut retval: v8::ReturnValue<v8::Value>,
|
|
) {
|
|
let value = if args.length() == 0 {
|
|
v8::undefined(scope).into()
|
|
} else {
|
|
args.get(0)
|
|
};
|
|
let text = match serialize_output_text(scope, value) {
|
|
Ok(text) => text,
|
|
Err(error_text) => {
|
|
throw_type_error(scope, &error_text);
|
|
return;
|
|
}
|
|
};
|
|
if let Some(state) = scope.get_slot::<RuntimeState>() {
|
|
let _ = state.event_tx.send(RuntimeEvent::ContentItem(
|
|
FunctionCallOutputContentItem::InputText { text },
|
|
));
|
|
}
|
|
retval.set(v8::undefined(scope).into());
|
|
}
|
|
|
|
pub(super) fn image_callback(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
args: v8::FunctionCallbackArguments,
|
|
mut retval: v8::ReturnValue<v8::Value>,
|
|
) {
|
|
let value = if args.length() == 0 {
|
|
v8::undefined(scope).into()
|
|
} else {
|
|
args.get(0)
|
|
};
|
|
let image_item = match normalize_output_image(scope, value) {
|
|
Ok(image_item) => image_item,
|
|
Err(()) => return,
|
|
};
|
|
if let Some(state) = scope.get_slot::<RuntimeState>() {
|
|
let _ = state.event_tx.send(RuntimeEvent::ContentItem(image_item));
|
|
}
|
|
retval.set(v8::undefined(scope).into());
|
|
}
|
|
|
|
pub(super) fn store_callback(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
args: v8::FunctionCallbackArguments,
|
|
_retval: v8::ReturnValue<v8::Value>,
|
|
) {
|
|
let key = match args.get(0).to_string(scope) {
|
|
Some(key) => key.to_rust_string_lossy(scope),
|
|
None => {
|
|
throw_type_error(scope, "store key must be a string");
|
|
return;
|
|
}
|
|
};
|
|
let value = args.get(1);
|
|
let serialized = match v8_value_to_json(scope, value) {
|
|
Ok(Some(value)) => value,
|
|
Ok(None) => {
|
|
throw_type_error(
|
|
scope,
|
|
&format!("Unable to store {key:?}. Only plain serializable objects can be stored."),
|
|
);
|
|
return;
|
|
}
|
|
Err(error_text) => {
|
|
throw_type_error(scope, &error_text);
|
|
return;
|
|
}
|
|
};
|
|
if let Some(state) = scope.get_slot_mut::<RuntimeState>() {
|
|
state.stored_values.insert(key, serialized);
|
|
}
|
|
}
|
|
|
|
pub(super) fn load_callback(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
args: v8::FunctionCallbackArguments,
|
|
mut retval: v8::ReturnValue<v8::Value>,
|
|
) {
|
|
let key = match args.get(0).to_string(scope) {
|
|
Some(key) => key.to_rust_string_lossy(scope),
|
|
None => {
|
|
throw_type_error(scope, "load key must be a string");
|
|
return;
|
|
}
|
|
};
|
|
let value = scope
|
|
.get_slot::<RuntimeState>()
|
|
.and_then(|state| state.stored_values.get(&key))
|
|
.cloned();
|
|
let Some(value) = value else {
|
|
retval.set(v8::undefined(scope).into());
|
|
return;
|
|
};
|
|
let Some(value) = json_to_v8(scope, &value) else {
|
|
throw_type_error(scope, "failed to load stored value");
|
|
return;
|
|
};
|
|
retval.set(value);
|
|
}
|
|
|
|
pub(super) fn notify_callback(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
args: v8::FunctionCallbackArguments,
|
|
mut retval: v8::ReturnValue<v8::Value>,
|
|
) {
|
|
let value = if args.length() == 0 {
|
|
v8::undefined(scope).into()
|
|
} else {
|
|
args.get(0)
|
|
};
|
|
let text = match serialize_output_text(scope, value) {
|
|
Ok(text) => text,
|
|
Err(error_text) => {
|
|
throw_type_error(scope, &error_text);
|
|
return;
|
|
}
|
|
};
|
|
if text.trim().is_empty() {
|
|
throw_type_error(scope, "notify expects non-empty text");
|
|
return;
|
|
}
|
|
if let Some(state) = scope.get_slot::<RuntimeState>() {
|
|
let _ = state.event_tx.send(RuntimeEvent::Notify {
|
|
call_id: state.tool_call_id.clone(),
|
|
text,
|
|
});
|
|
}
|
|
retval.set(v8::undefined(scope).into());
|
|
}
|
|
|
|
pub(super) fn yield_control_callback(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
_args: v8::FunctionCallbackArguments,
|
|
_retval: v8::ReturnValue<v8::Value>,
|
|
) {
|
|
if let Some(state) = scope.get_slot::<RuntimeState>() {
|
|
let _ = state.event_tx.send(RuntimeEvent::YieldRequested);
|
|
}
|
|
}
|
|
|
|
pub(super) fn exit_callback(
|
|
scope: &mut v8::PinScope<'_, '_>,
|
|
_args: v8::FunctionCallbackArguments,
|
|
_retval: v8::ReturnValue<v8::Value>,
|
|
) {
|
|
if let Some(state) = scope.get_slot_mut::<RuntimeState>() {
|
|
state.exit_requested = true;
|
|
}
|
|
if let Some(error) = v8::String::new(scope, EXIT_SENTINEL) {
|
|
scope.throw_exception(error.into());
|
|
}
|
|
}
|