mirror of
https://github.com/openai/codex.git
synced 2026-09-14 11:57:03 +00:00
Link code_mode to rusty_v8
This commit is contained in:
19
MODULE.bazel.lock
generated
19
MODULE.bazel.lock
generated
File diff suppressed because one or more lines are too long
229
codex-rs/Cargo.lock
generated
229
codex-rs/Cargo.lock
generated
@@ -948,6 +948,8 @@ dependencies = [
|
||||
"cexpr",
|
||||
"clang-sys",
|
||||
"itertools 0.13.0",
|
||||
"log",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
@@ -1151,6 +1153,16 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
|
||||
|
||||
[[package]]
|
||||
name = "calendrical_calculations"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a0b39595c6ee54a8d0900204ba4c401d0ab4eb45adaf07178e8d017541529e7"
|
||||
dependencies = [
|
||||
"core_maths",
|
||||
"displaydoc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cassowary"
|
||||
version = "0.3.0"
|
||||
@@ -1572,7 +1584,7 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
"url",
|
||||
"which",
|
||||
"which 8.0.0",
|
||||
"wiremock",
|
||||
"zip",
|
||||
]
|
||||
@@ -1893,8 +1905,9 @@ dependencies = [
|
||||
"tracing-test",
|
||||
"url",
|
||||
"uuid",
|
||||
"v8",
|
||||
"walkdir",
|
||||
"which",
|
||||
"which 8.0.0",
|
||||
"wildmatch",
|
||||
"windows-sys 0.52.0",
|
||||
"wiremock",
|
||||
@@ -2105,7 +2118,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"which",
|
||||
"which 8.0.0",
|
||||
"wiremock",
|
||||
]
|
||||
|
||||
@@ -2343,7 +2356,7 @@ dependencies = [
|
||||
"tracing",
|
||||
"urlencoding",
|
||||
"webbrowser",
|
||||
"which",
|
||||
"which 8.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2383,7 +2396,7 @@ dependencies = [
|
||||
"tree-sitter",
|
||||
"tree-sitter-bash",
|
||||
"url",
|
||||
"which",
|
||||
"which 8.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2542,7 +2555,7 @@ dependencies = [
|
||||
"uuid",
|
||||
"vt100",
|
||||
"webbrowser",
|
||||
"which",
|
||||
"which 8.0.0",
|
||||
"windows-sys 0.52.0",
|
||||
"winsplit",
|
||||
]
|
||||
@@ -2916,6 +2929,15 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core_maths"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core_test_support"
|
||||
version = "0.0.0"
|
||||
@@ -3513,6 +3535,38 @@ dependencies = [
|
||||
"subtle",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diplomat"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9adb46b05e2f53dcf6a7dfc242e4ce9eb60c369b6b6eb10826a01e93167f59c6"
|
||||
dependencies = [
|
||||
"diplomat_core",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diplomat-runtime"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0569bd3caaf13829da7ee4e83dbf9197a0e1ecd72772da6d08f0b4c9285c8d29"
|
||||
|
||||
[[package]]
|
||||
name = "diplomat_core"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51731530ed7f2d4495019abc7df3744f53338e69e2863a6a64ae91821c763df1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"smallvec",
|
||||
"strck",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "6.0.0"
|
||||
@@ -4119,6 +4173,16 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fslock"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04412b8935272e3a9bae6f48c7bfff74c2911f60525404edfdd28e49884c3bfb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
@@ -4347,6 +4411,15 @@ dependencies = [
|
||||
"regex-syntax 0.8.8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gzip-header"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95cc527b92e6029a62960ad99aa8a6660faa4555fe5f731aab13aa6a921795a2"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "0.4.13"
|
||||
@@ -4804,6 +4877,28 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_calendar"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f0e52e009b6b16ba9c0693578796f2dd4aaa59a7f8f920423706714a89ac4e"
|
||||
dependencies = [
|
||||
"calendrical_calculations",
|
||||
"displaydoc",
|
||||
"icu_calendar_data",
|
||||
"icu_locale",
|
||||
"icu_locale_core",
|
||||
"icu_provider",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_calendar_data"
|
||||
version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "527f04223b17edfe0bd43baf14a0cb1b017830db65f3950dc00224860a9a446d"
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
@@ -5238,6 +5333,12 @@ version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "ixdtf"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84de9d95a6d2547d9b77ee3f25fa0ee32e3c3a6484d47a55adebc0439c077992"
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.18"
|
||||
@@ -6962,6 +7063,16 @@ dependencies = [
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn 2.0.114",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.4.0"
|
||||
@@ -7794,6 +7905,16 @@ dependencies = [
|
||||
"webpki-roots 1.0.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resb"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a067ab3b5ca3b4dc307d0de9cf75f9f5e6ca9717b192b2f28a36c83e5de9e76"
|
||||
dependencies = [
|
||||
"potential_utf",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "resolv-conf"
|
||||
version = "0.7.6"
|
||||
@@ -9174,6 +9295,15 @@ dependencies = [
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strck"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42316e70da376f3d113a68d138a60d8a9883c604fe97942721ec2068dab13a9f"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "streaming-iterator"
|
||||
version = "0.1.9"
|
||||
@@ -9403,6 +9533,39 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "temporal_capi"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a151e402c2bdb6a3a2a2f3f225eddaead2e7ce7dd5d3fa2090deb11b17aa4ed8"
|
||||
dependencies = [
|
||||
"diplomat",
|
||||
"diplomat-runtime",
|
||||
"icu_calendar",
|
||||
"icu_locale",
|
||||
"num-traits",
|
||||
"temporal_rs",
|
||||
"timezone_provider",
|
||||
"writeable",
|
||||
"zoneinfo64",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "temporal_rs"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88afde3bd75d2fc68d77a914bece426aa08aa7649ffd0cdd4a11c3d4d33474d1"
|
||||
dependencies = [
|
||||
"core_maths",
|
||||
"icu_calendar",
|
||||
"icu_locale",
|
||||
"ixdtf",
|
||||
"num-traits",
|
||||
"timezone_provider",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "term"
|
||||
version = "0.7.0"
|
||||
@@ -9610,6 +9773,18 @@ dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "timezone_provider"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df9ba0000e9e73862f3e7ca1ff159e2ddf915c9d8bb11e38a7874760f445d993"
|
||||
dependencies = [
|
||||
"tinystr",
|
||||
"zerotrie",
|
||||
"zerovec",
|
||||
"zoneinfo64",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
@@ -10409,6 +10584,23 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "v8"
|
||||
version = "146.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d97bcac5cdc5a195a4813f1855a6bc658f240452aac36caa12fd6c6f16026ab1"
|
||||
dependencies = [
|
||||
"bindgen",
|
||||
"bitflags 2.10.0",
|
||||
"fslock",
|
||||
"gzip-header",
|
||||
"home",
|
||||
"miniz_oxide",
|
||||
"paste",
|
||||
"temporal_capi",
|
||||
"which 6.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
@@ -10708,6 +10900,18 @@ version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "6.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"home",
|
||||
"rustix 0.38.44",
|
||||
"winsafe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "which"
|
||||
version = "8.0.0"
|
||||
@@ -11711,6 +11915,19 @@ version = "1.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
|
||||
|
||||
[[package]]
|
||||
name = "zoneinfo64"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb2e5597efbe7c421da8a7fd396b20b571704e787c21a272eecf35dfe9d386f0"
|
||||
dependencies = [
|
||||
"calendrical_calculations",
|
||||
"icu_locale_core",
|
||||
"potential_utf",
|
||||
"resb",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zopfli"
|
||||
version = "0.8.3"
|
||||
|
||||
@@ -234,6 +234,7 @@ regex-lite = "0.1.8"
|
||||
reqwest = "0.12"
|
||||
rmcp = { version = "0.15.0", default-features = false }
|
||||
runfiles = { git = "https://github.com/dzbarsky/rules_rust", rev = "b56cbaa8465e74127f1ea216f813cd377295ad81" }
|
||||
rusty_v8 = { package = "v8", version = "146.4.0" }
|
||||
rustls = { version = "0.23", default-features = false, features = [
|
||||
"ring",
|
||||
"std",
|
||||
|
||||
@@ -81,6 +81,7 @@ rmcp = { workspace = true, default-features = false, features = [
|
||||
"schemars",
|
||||
"server",
|
||||
] }
|
||||
rusty_v8 = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
@@ -405,17 +405,6 @@ impl Codex {
|
||||
warn!("{message}");
|
||||
config.startup_warnings.push(message);
|
||||
}
|
||||
if config.features.enabled(Feature::CodeMode)
|
||||
&& let Err(err) = resolve_compatible_node(config.js_repl_node_path.as_deref()).await
|
||||
{
|
||||
let message = format!(
|
||||
"Disabled `code_mode` for this session because the configured Node runtime is unavailable or incompatible. {err}"
|
||||
);
|
||||
warn!("{message}");
|
||||
let _ = config.features.disable(Feature::CodeMode);
|
||||
config.startup_warnings.push(message);
|
||||
}
|
||||
|
||||
let allowed_skills_for_implicit_invocation =
|
||||
loaded_skills.allowed_skills_for_implicit_invocation();
|
||||
let user_instructions = get_user_instructions(
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
use std::process::ExitStatus;
|
||||
use std::pin::pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Once;
|
||||
|
||||
use crate::client_common::tools::ToolSpec;
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::config::Config;
|
||||
use crate::exec_env::create_env;
|
||||
use crate::features::Feature;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::tools::ToolRouter;
|
||||
use crate::tools::context::SharedTurnDiffTracker;
|
||||
use crate::tools::context::ToolPayload;
|
||||
use crate::tools::js_repl::resolve_compatible_node;
|
||||
use crate::tools::router::ToolCall;
|
||||
use crate::tools::router::ToolCallSource;
|
||||
use codex_protocol::models::ContentItem;
|
||||
@@ -19,17 +18,20 @@ use codex_protocol::models::FunctionCallOutputBody;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use rusty_v8 as v8;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value as JsonValue;
|
||||
use serde_json::json;
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::io::BufReader;
|
||||
use tokio::runtime::Handle;
|
||||
use tokio::runtime::RuntimeFlavor;
|
||||
|
||||
const CODE_MODE_RUNNER_SOURCE: &str = include_str!("code_mode_runner.cjs");
|
||||
const CODE_MODE_BRIDGE_SOURCE: &str = include_str!("code_mode_bridge.js");
|
||||
const CODE_MODE_BOOTSTRAP_SOURCE: &str = include_str!("code_mode_bridge.js");
|
||||
const CODE_MODE_BOOTSTRAP_FILENAME: &str = "code_mode_bootstrap.js";
|
||||
const CODE_MODE_MAIN_FILENAME: &str = "code_mode_main.mjs";
|
||||
const CODE_MODE_TOOLS_MODULE_NAME: &str = "tools.js";
|
||||
|
||||
static CODE_MODE_V8_INIT: Once = Once::new();
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ExecContext {
|
||||
@@ -38,6 +40,12 @@ struct ExecContext {
|
||||
tracker: SharedTurnDiffTracker,
|
||||
}
|
||||
|
||||
struct CodeModeRuntimeState {
|
||||
exec: ExecContext,
|
||||
enabled_tools: Vec<EnabledTool>,
|
||||
tools_module: Option<v8::Global<v8::Module>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum CodeModeToolKind {
|
||||
@@ -51,45 +59,15 @@ struct EnabledTool {
|
||||
kind: CodeModeToolKind,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum HostToNodeMessage {
|
||||
Init {
|
||||
enabled_tools: Vec<EnabledTool>,
|
||||
source: String,
|
||||
},
|
||||
Response {
|
||||
id: String,
|
||||
content_items: Vec<JsonValue>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum NodeToHostMessage {
|
||||
ToolCall {
|
||||
id: String,
|
||||
name: String,
|
||||
#[serde(default)]
|
||||
input: Option<JsonValue>,
|
||||
},
|
||||
Result {
|
||||
content_items: Vec<JsonValue>,
|
||||
},
|
||||
}
|
||||
|
||||
pub(crate) fn instructions(config: &Config) -> Option<String> {
|
||||
if !config.features.enabled(Feature::CodeMode) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut section = String::from("## Code Mode\n");
|
||||
section.push_str(
|
||||
"- Use `code_mode` for JavaScript execution in a Node-backed `node:vm` context.\n",
|
||||
);
|
||||
section.push_str("- Use `code_mode` for JavaScript execution in an embedded V8 runtime.\n");
|
||||
section.push_str("- `code_mode` is a freeform/custom tool. Direct `code_mode` calls must send raw JavaScript tool input. Do not wrap code in JSON, quotes, or markdown code fences.\n");
|
||||
section.push_str("- Direct tool calls remain available while `code_mode` is enabled.\n");
|
||||
section.push_str("- `code_mode` uses the same Node runtime resolution as `js_repl`. If needed, point `js_repl_node_path` at the Node binary you want Codex to use.\n");
|
||||
section.push_str("- Import nested tools from `tools.js`, for example `import { exec_command } from \"tools.js\"` or `import { tools } from \"tools.js\"`. `tools[name]` and identifier wrappers like `await exec_command(args)` remain available for compatibility. Nested tool calls resolve to arrays of content items.\n");
|
||||
section.push_str(
|
||||
"- Function tools require JSON object arguments. Freeform tools require raw strings.\n",
|
||||
@@ -112,156 +90,530 @@ pub(crate) async fn execute(
|
||||
tracker,
|
||||
};
|
||||
let enabled_tools = build_enabled_tools(&exec);
|
||||
let source = build_source(&code, &enabled_tools).map_err(FunctionCallError::RespondToModel)?;
|
||||
execute_node(exec, source, enabled_tools)
|
||||
.await
|
||||
let bootstrap_source =
|
||||
build_bootstrap_source(&enabled_tools).map_err(FunctionCallError::RespondToModel)?;
|
||||
execute_v8(exec, code, enabled_tools, bootstrap_source)
|
||||
.map_err(FunctionCallError::RespondToModel)
|
||||
}
|
||||
|
||||
async fn execute_node(
|
||||
fn execute_v8(
|
||||
exec: ExecContext,
|
||||
source: String,
|
||||
code: String,
|
||||
enabled_tools: Vec<EnabledTool>,
|
||||
bootstrap_source: String,
|
||||
) -> Result<Vec<FunctionCallOutputContentItem>, String> {
|
||||
let node_path = resolve_compatible_node(exec.turn.config.js_repl_node_path.as_deref()).await?;
|
||||
init_v8();
|
||||
|
||||
let env = create_env(&exec.turn.shell_environment_policy, None);
|
||||
let mut cmd = tokio::process::Command::new(&node_path);
|
||||
cmd.arg("--experimental-vm-modules");
|
||||
cmd.arg("--eval");
|
||||
cmd.arg(CODE_MODE_RUNNER_SOURCE);
|
||||
cmd.current_dir(&exec.turn.cwd);
|
||||
cmd.env_clear();
|
||||
cmd.envs(env);
|
||||
cmd.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true);
|
||||
|
||||
let mut child = cmd
|
||||
.spawn()
|
||||
.map_err(|err| format!("failed to start code_mode Node runtime: {err}"))?;
|
||||
let stdout = child
|
||||
.stdout
|
||||
.take()
|
||||
.ok_or_else(|| "code_mode runner missing stdout".to_string())?;
|
||||
let stderr = child
|
||||
.stderr
|
||||
.take()
|
||||
.ok_or_else(|| "code_mode runner missing stderr".to_string())?;
|
||||
let mut stdin = child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or_else(|| "code_mode runner missing stdin".to_string())?;
|
||||
|
||||
let stderr_task = tokio::spawn(async move {
|
||||
let mut reader = BufReader::new(stderr);
|
||||
let mut buf = Vec::new();
|
||||
let _ = reader.read_to_end(&mut buf).await;
|
||||
String::from_utf8_lossy(&buf).trim().to_string()
|
||||
let mut isolate = v8::Isolate::new(v8::CreateParams::default());
|
||||
isolate.set_capture_stack_trace_for_uncaught_exceptions(true, 32);
|
||||
isolate.set_host_import_module_dynamically_callback(code_mode_dynamic_import_callback);
|
||||
isolate.set_slot(CodeModeRuntimeState {
|
||||
exec,
|
||||
enabled_tools: enabled_tools.clone(),
|
||||
tools_module: None,
|
||||
});
|
||||
|
||||
write_message(
|
||||
&mut stdin,
|
||||
&HostToNodeMessage::Init {
|
||||
enabled_tools: enabled_tools.clone(),
|
||||
source,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let scope = pin!(v8::HandleScope::new(&mut isolate));
|
||||
let scope = &mut scope.init();
|
||||
let context = v8::Context::new(scope, Default::default());
|
||||
let scope = &mut v8::ContextScope::new(scope, context);
|
||||
|
||||
let mut stdout_lines = BufReader::new(stdout).lines();
|
||||
let mut final_content_items = None;
|
||||
while let Some(line) = stdout_lines
|
||||
.next_line()
|
||||
.await
|
||||
.map_err(|err| format!("failed to read code_mode runner stdout: {err}"))?
|
||||
{
|
||||
if line.trim().is_empty() {
|
||||
install_tool_call_binding(scope)?;
|
||||
run_script(scope, CODE_MODE_BOOTSTRAP_FILENAME, &bootstrap_source)?;
|
||||
|
||||
let tools_module = create_tools_module(scope, &enabled_tools)?;
|
||||
let tools_module = v8::Global::new(scope, tools_module);
|
||||
let Some(runtime_state) = scope.get_slot_mut::<CodeModeRuntimeState>() else {
|
||||
return Err("code_mode runtime state missing".to_string());
|
||||
};
|
||||
runtime_state.tools_module = Some(tools_module);
|
||||
|
||||
let scope = pin!(v8::TryCatch::new(scope));
|
||||
let scope = &mut scope.init();
|
||||
let filename = v8_string(scope, CODE_MODE_MAIN_FILENAME)?;
|
||||
let source = v8_string(scope, &code)?;
|
||||
let origin = script_origin(scope, filename, true);
|
||||
let mut source = v8::script_compiler::Source::new(source, Some(&origin));
|
||||
let Some(module) = v8::script_compiler::compile_module(scope, &mut source) else {
|
||||
return Err(format_v8_exception(scope));
|
||||
};
|
||||
let Some(instantiated) = module.instantiate_module(scope, resolve_code_mode_module) else {
|
||||
return Err(format_v8_exception(scope));
|
||||
};
|
||||
if !instantiated {
|
||||
return Err("failed to instantiate code_mode module".to_string());
|
||||
}
|
||||
|
||||
let Some(result) = module.evaluate(scope) else {
|
||||
return Err(format_v8_exception(scope));
|
||||
};
|
||||
if result.is_promise() {
|
||||
let promise = v8::Local::<v8::Promise>::try_from(result)
|
||||
.map_err(|_| "code_mode module evaluation did not return a promise".to_string())?;
|
||||
wait_for_module_promise(scope, module, promise)?;
|
||||
} else {
|
||||
scope.perform_microtask_checkpoint();
|
||||
}
|
||||
|
||||
read_content_items(scope)
|
||||
}
|
||||
|
||||
fn init_v8() {
|
||||
CODE_MODE_V8_INIT.call_once(|| {
|
||||
let platform = v8::new_default_platform(0, false).make_shared();
|
||||
v8::V8::initialize_platform(platform);
|
||||
v8::V8::initialize();
|
||||
});
|
||||
}
|
||||
|
||||
fn install_tool_call_binding(scope: &mut v8::PinScope<'_, '_>) -> Result<(), String> {
|
||||
let function = v8::Function::new(scope, code_mode_tool_call_callback)
|
||||
.ok_or_else(|| "failed to install code_mode tool bridge".to_string())?;
|
||||
let key = v8_string(scope, "__codex_tool_call")?;
|
||||
let global = scope.get_current_context().global(scope);
|
||||
if global.set(scope, key.into(), function.into()).is_some() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("failed to bind __codex_tool_call".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn run_script(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
filename: &str,
|
||||
source: &str,
|
||||
) -> Result<(), String> {
|
||||
let scope = pin!(v8::TryCatch::new(scope));
|
||||
let scope = &mut scope.init();
|
||||
let source = v8_string(scope, source)?;
|
||||
let filename = v8_string(scope, filename)?;
|
||||
let origin = script_origin(scope, filename, false);
|
||||
let Some(script) = v8::Script::compile(scope, source, Some(&origin)) else {
|
||||
return Err(format_v8_exception(scope));
|
||||
};
|
||||
if script.run(scope).is_none() {
|
||||
return Err(format_v8_exception(scope));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn script_origin<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
filename: v8::Local<'s, v8::String>,
|
||||
is_module: bool,
|
||||
) -> v8::ScriptOrigin<'s> {
|
||||
v8::ScriptOrigin::new(
|
||||
scope,
|
||||
filename.into(),
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
None,
|
||||
false,
|
||||
false,
|
||||
is_module,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn create_tools_module<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
enabled_tools: &[EnabledTool],
|
||||
) -> Result<v8::Local<'s, v8::Module>, String> {
|
||||
let mut export_names = vec![v8_string(scope, "tools")?];
|
||||
for tool in enabled_tools {
|
||||
if tool.name != "tools" && is_valid_identifier(&tool.name) {
|
||||
export_names.push(v8_string(scope, &tool.name)?);
|
||||
}
|
||||
}
|
||||
let module_name = v8_string(scope, CODE_MODE_TOOLS_MODULE_NAME)?;
|
||||
Ok(v8::Module::create_synthetic_module(
|
||||
scope,
|
||||
module_name,
|
||||
&export_names,
|
||||
evaluate_tools_module,
|
||||
))
|
||||
}
|
||||
|
||||
fn evaluate_tools_module<'s>(
|
||||
context: v8::Local<'s, v8::Context>,
|
||||
module: v8::Local<'s, v8::Module>,
|
||||
) -> Option<v8::Local<'s, v8::Value>> {
|
||||
v8::callback_scope!(unsafe scope, context);
|
||||
let Some(runtime_state) = scope.get_slot::<CodeModeRuntimeState>() else {
|
||||
return throw_v8_exception(scope, "code_mode runtime state missing");
|
||||
};
|
||||
let enabled_tools = runtime_state.enabled_tools.clone();
|
||||
let global = context.global(scope);
|
||||
|
||||
let tools_name = match v8::String::new(scope, "tools") {
|
||||
Some(name) => name,
|
||||
None => return throw_v8_exception(scope, "failed to allocate tools export name"),
|
||||
};
|
||||
let Some(tools_value) = global.get(scope, tools_name.into()) else {
|
||||
return throw_v8_exception(scope, "code_mode tools namespace missing");
|
||||
};
|
||||
let Ok(tools_object) = v8::Local::<v8::Object>::try_from(tools_value) else {
|
||||
return throw_v8_exception(scope, "code_mode tools namespace is not an object");
|
||||
};
|
||||
module.set_synthetic_module_export(scope, tools_name, tools_object.into())?;
|
||||
|
||||
for tool in enabled_tools {
|
||||
if !is_valid_identifier(&tool.name) || tool.name == "tools" {
|
||||
continue;
|
||||
}
|
||||
let message: NodeToHostMessage = serde_json::from_str(&line)
|
||||
.map_err(|err| format!("invalid code_mode runner message: {err}; line={line}"))?;
|
||||
match message {
|
||||
NodeToHostMessage::ToolCall { id, name, input } => {
|
||||
let response = HostToNodeMessage::Response {
|
||||
id,
|
||||
content_items: call_nested_tool(exec.clone(), name, input).await,
|
||||
};
|
||||
write_message(&mut stdin, &response).await?;
|
||||
let Some(export_name) = v8::String::new(scope, &tool.name) else {
|
||||
return throw_v8_exception(scope, "failed to allocate tool export name");
|
||||
};
|
||||
let Some(export_value) = tools_object.get(scope, export_name.into()) else {
|
||||
return throw_v8_exception(
|
||||
scope,
|
||||
&format!("code_mode tool export `{}` is unavailable", tool.name),
|
||||
);
|
||||
};
|
||||
module.set_synthetic_module_export(scope, export_name, export_value)?;
|
||||
}
|
||||
|
||||
Some(v8::undefined(scope).into())
|
||||
}
|
||||
|
||||
fn resolve_code_mode_module<'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);
|
||||
match resolve_tools_module(scope, specifier) {
|
||||
Ok(module) => Some(module),
|
||||
Err(error) => throw_v8_exception(scope, &error),
|
||||
}
|
||||
}
|
||||
|
||||
fn code_mode_dynamic_import_callback<'s, 'i>(
|
||||
scope: &mut v8::PinScope<'s, 'i>,
|
||||
_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 resolver = v8::PromiseResolver::new(scope)?;
|
||||
let promise = resolver.get_promise(scope);
|
||||
|
||||
match resolve_tools_module(scope, specifier).and_then(|module| {
|
||||
instantiate_and_evaluate_tools_module(scope, module)?;
|
||||
Ok(module.get_module_namespace())
|
||||
}) {
|
||||
Ok(namespace) => {
|
||||
let _ = resolver.resolve(scope, namespace);
|
||||
}
|
||||
Err(error) => {
|
||||
let error = v8_string(scope, &error).ok()?;
|
||||
let _ = resolver.reject(scope, error.into());
|
||||
}
|
||||
}
|
||||
|
||||
Some(promise)
|
||||
}
|
||||
|
||||
fn resolve_tools_module<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
specifier: v8::Local<'s, v8::String>,
|
||||
) -> Result<v8::Local<'s, v8::Module>, String> {
|
||||
let specifier = specifier.to_rust_string_lossy(scope);
|
||||
if specifier != CODE_MODE_TOOLS_MODULE_NAME {
|
||||
return Err(format!("Unsupported import in code_mode: {specifier}"));
|
||||
}
|
||||
|
||||
let Some(runtime_state) = scope.get_slot::<CodeModeRuntimeState>() else {
|
||||
return Err("code_mode runtime state missing".to_string());
|
||||
};
|
||||
let Some(tools_module) = runtime_state.tools_module.as_ref() else {
|
||||
return Err("code_mode tools module missing".to_string());
|
||||
};
|
||||
Ok(v8::Local::new(scope, tools_module))
|
||||
}
|
||||
|
||||
fn instantiate_and_evaluate_tools_module(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
module: v8::Local<'_, v8::Module>,
|
||||
) -> Result<(), String> {
|
||||
match module.get_status() {
|
||||
v8::ModuleStatus::Uninstantiated => {
|
||||
let Some(instantiated) = module.instantiate_module(scope, resolve_code_mode_module)
|
||||
else {
|
||||
return Err("failed to instantiate code_mode tools module".to_string());
|
||||
};
|
||||
if !instantiated {
|
||||
return Err("failed to instantiate code_mode tools module".to_string());
|
||||
}
|
||||
NodeToHostMessage::Result { content_items } => {
|
||||
final_content_items = Some(output_content_items_from_json_values(content_items)?);
|
||||
break;
|
||||
}
|
||||
v8::ModuleStatus::Instantiating => {
|
||||
return Err("code_mode tools module is already instantiating".to_string());
|
||||
}
|
||||
v8::ModuleStatus::Instantiated
|
||||
| v8::ModuleStatus::Evaluating
|
||||
| v8::ModuleStatus::Evaluated => {}
|
||||
v8::ModuleStatus::Errored => {
|
||||
return Err(format_v8_value(scope, module.get_exception()));
|
||||
}
|
||||
}
|
||||
|
||||
match module.get_status() {
|
||||
v8::ModuleStatus::Instantiated => {
|
||||
let Some(result) = module.evaluate(scope) else {
|
||||
return Err("failed to evaluate code_mode tools module".to_string());
|
||||
};
|
||||
if result.is_promise() {
|
||||
let promise = v8::Local::<v8::Promise>::try_from(result).map_err(|_| {
|
||||
"code_mode tools module evaluation did not return a promise".to_string()
|
||||
})?;
|
||||
scope.perform_microtask_checkpoint();
|
||||
match promise.state() {
|
||||
v8::PromiseState::Fulfilled => {}
|
||||
v8::PromiseState::Rejected => {
|
||||
return Err(format_v8_value(scope, promise.result(scope)));
|
||||
}
|
||||
v8::PromiseState::Pending => {
|
||||
return Err("code_mode tools module evaluation did not settle".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
v8::ModuleStatus::Evaluated => {}
|
||||
v8::ModuleStatus::Evaluating => {
|
||||
return Err("code_mode tools module is already evaluating".to_string());
|
||||
}
|
||||
v8::ModuleStatus::Errored => {
|
||||
return Err(format_v8_value(scope, module.get_exception()));
|
||||
}
|
||||
v8::ModuleStatus::Uninstantiated | v8::ModuleStatus::Instantiating => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn code_mode_tool_call_callback(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
args: v8::FunctionCallbackArguments,
|
||||
mut rv: v8::ReturnValue<v8::Value>,
|
||||
) {
|
||||
let Some(resolver) = v8::PromiseResolver::new(scope) else {
|
||||
return;
|
||||
};
|
||||
let promise = resolver.get_promise(scope);
|
||||
rv.set(promise.into());
|
||||
|
||||
let result = run_tool_call(scope, &args).and_then(|value| json_to_v8(scope, &value));
|
||||
match result {
|
||||
Ok(value) => {
|
||||
let _ = resolver.resolve(scope, value);
|
||||
}
|
||||
Err(error) => {
|
||||
if let Some(error) = v8::String::new(scope, &error) {
|
||||
let _ = resolver.reject(scope, error.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_tool_call(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
args: &v8::FunctionCallbackArguments,
|
||||
) -> Result<JsonValue, String> {
|
||||
let tool_name = args
|
||||
.get(0)
|
||||
.to_string(scope)
|
||||
.ok_or_else(|| "code_mode tool call requires a tool name".to_string())?
|
||||
.to_rust_string_lossy(scope);
|
||||
let input = json_from_v8(scope, args.get(1))?;
|
||||
|
||||
let Some(exec) = scope
|
||||
.get_slot::<CodeModeRuntimeState>()
|
||||
.map(|runtime_state| runtime_state.exec.clone())
|
||||
else {
|
||||
return Err("code_mode runtime state missing".to_string());
|
||||
};
|
||||
|
||||
let content_items = match Handle::current().runtime_flavor() {
|
||||
RuntimeFlavor::MultiThread => tokio::task::block_in_place(|| {
|
||||
Handle::current().block_on(call_nested_tool(exec, tool_name, input))
|
||||
}),
|
||||
RuntimeFlavor::CurrentThread => {
|
||||
return Err("code_mode tool calls require a multi-thread Tokio runtime".to_string());
|
||||
}
|
||||
_ => {
|
||||
return Err("code_mode tool calls require a supported Tokio runtime".to_string());
|
||||
}
|
||||
};
|
||||
|
||||
Ok(JsonValue::Array(content_items))
|
||||
}
|
||||
|
||||
fn wait_for_module_promise(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
module: v8::Local<'_, v8::Module>,
|
||||
promise: v8::Local<'_, v8::Promise>,
|
||||
) -> Result<(), String> {
|
||||
for _ in 0..32 {
|
||||
match promise.state() {
|
||||
v8::PromiseState::Fulfilled => return Ok(()),
|
||||
v8::PromiseState::Rejected => {
|
||||
return Err(format_v8_value(scope, promise.result(scope)));
|
||||
}
|
||||
v8::PromiseState::Pending => {
|
||||
scope.perform_microtask_checkpoint();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(stdin);
|
||||
|
||||
let status = child
|
||||
.wait()
|
||||
.await
|
||||
.map_err(|err| format!("failed to wait for code_mode runner: {err}"))?;
|
||||
let stderr = stderr_task
|
||||
.await
|
||||
.map_err(|err| format!("failed to collect code_mode stderr: {err}"))?;
|
||||
|
||||
match final_content_items {
|
||||
Some(content_items) if status.success() => Ok(content_items),
|
||||
Some(_) => Err(format_runner_failure(
|
||||
"code_mode execution failed",
|
||||
status,
|
||||
&stderr,
|
||||
)),
|
||||
None => Err(format_runner_failure(
|
||||
"code_mode runner exited without returning a result",
|
||||
status,
|
||||
&stderr,
|
||||
)),
|
||||
let stalled = module.get_stalled_top_level_await_message(scope);
|
||||
if let Some((_module, message)) = stalled.into_iter().next() {
|
||||
let pending = message.get(scope).to_rust_string_lossy(scope);
|
||||
let filename = message
|
||||
.get_script_resource_name(scope)
|
||||
.map(|name| name.to_rust_string_lossy(scope))
|
||||
.unwrap_or_else(|| CODE_MODE_MAIN_FILENAME.to_string());
|
||||
let line = message.get_line_number(scope).unwrap_or_default();
|
||||
return Err(format!("{filename}:{line}: {pending}"));
|
||||
}
|
||||
|
||||
Err("code_mode top-level await did not settle".to_string())
|
||||
}
|
||||
|
||||
async fn write_message(
|
||||
stdin: &mut tokio::process::ChildStdin,
|
||||
message: &HostToNodeMessage,
|
||||
) -> Result<(), String> {
|
||||
let line = serde_json::to_string(message)
|
||||
.map_err(|err| format!("failed to serialize code_mode message: {err}"))?;
|
||||
stdin
|
||||
.write_all(line.as_bytes())
|
||||
.await
|
||||
.map_err(|err| format!("failed to write code_mode message: {err}"))?;
|
||||
stdin
|
||||
.write_all(b"\n")
|
||||
.await
|
||||
.map_err(|err| format!("failed to write code_mode message newline: {err}"))?;
|
||||
stdin
|
||||
.flush()
|
||||
.await
|
||||
.map_err(|err| format!("failed to flush code_mode message: {err}"))
|
||||
fn read_content_items(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
) -> Result<Vec<FunctionCallOutputContentItem>, String> {
|
||||
let source = v8_string(
|
||||
scope,
|
||||
"JSON.stringify(globalThis.__codexContentItems ?? [])",
|
||||
)?;
|
||||
let script = v8::Script::compile(scope, source, None)
|
||||
.ok_or_else(|| "failed to read code_mode content items".to_string())?;
|
||||
let value = script
|
||||
.run(scope)
|
||||
.ok_or_else(|| "failed to evaluate code_mode content items".to_string())?;
|
||||
let serialized = value
|
||||
.to_string(scope)
|
||||
.ok_or_else(|| "failed to serialize code_mode content items".to_string())?
|
||||
.to_rust_string_lossy(scope);
|
||||
let content_items = serde_json::from_str::<Vec<JsonValue>>(&serialized)
|
||||
.map_err(|err| format!("invalid code_mode content items: {err}"))?;
|
||||
output_content_items_from_json_values(content_items)
|
||||
}
|
||||
|
||||
fn append_stderr(message: String, stderr: &str) -> String {
|
||||
if stderr.trim().is_empty() {
|
||||
return message;
|
||||
}
|
||||
format!("{message}\n\nnode stderr:\n{stderr}")
|
||||
}
|
||||
|
||||
fn format_runner_failure(message: &str, status: ExitStatus, stderr: &str) -> String {
|
||||
append_stderr(format!("{message} (status {status})"), stderr)
|
||||
}
|
||||
|
||||
fn build_source(user_code: &str, enabled_tools: &[EnabledTool]) -> Result<String, String> {
|
||||
fn build_bootstrap_source(enabled_tools: &[EnabledTool]) -> Result<String, String> {
|
||||
let enabled_tools_json = serde_json::to_string(enabled_tools)
|
||||
.map_err(|err| format!("failed to serialize enabled tools: {err}"))?;
|
||||
Ok(CODE_MODE_BRIDGE_SOURCE
|
||||
.replace(
|
||||
"__CODE_MODE_ENABLED_TOOLS_PLACEHOLDER__",
|
||||
&enabled_tools_json,
|
||||
)
|
||||
.replace("__CODE_MODE_USER_CODE_PLACEHOLDER__", user_code))
|
||||
Ok(CODE_MODE_BOOTSTRAP_SOURCE.replace(
|
||||
"__CODE_MODE_ENABLED_TOOLS_PLACEHOLDER__",
|
||||
&enabled_tools_json,
|
||||
))
|
||||
}
|
||||
|
||||
fn v8_string<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
text: &str,
|
||||
) -> Result<v8::Local<'s, v8::String>, String> {
|
||||
v8::String::new(scope, text).ok_or_else(|| "failed to allocate V8 string".to_string())
|
||||
}
|
||||
|
||||
fn json_from_v8(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
value: v8::Local<'_, v8::Value>,
|
||||
) -> Result<Option<JsonValue>, String> {
|
||||
if value.is_undefined() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let Some(serialized) = v8::json::stringify(scope, value) else {
|
||||
return Err("code_mode tool arguments must be JSON-serializable".to_string());
|
||||
};
|
||||
let serialized = serialized.to_rust_string_lossy(scope);
|
||||
serde_json::from_str(&serialized)
|
||||
.map(Some)
|
||||
.map_err(|err| format!("invalid code_mode tool arguments: {err}"))
|
||||
}
|
||||
|
||||
fn json_to_v8<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
value: &JsonValue,
|
||||
) -> Result<v8::Local<'s, v8::Value>, String> {
|
||||
let serialized = serde_json::to_string(value)
|
||||
.map_err(|err| format!("failed to serialize code_mode tool response: {err}"))?;
|
||||
let serialized = v8_string(scope, &serialized)?;
|
||||
v8::json::parse(scope, serialized)
|
||||
.ok_or_else(|| "failed to deserialize code_mode tool response into V8".to_string())
|
||||
}
|
||||
|
||||
fn throw_v8_exception<'s, T>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
message: &str,
|
||||
) -> Option<v8::Local<'s, T>> {
|
||||
if let Some(message) = v8::String::new(scope, message) {
|
||||
scope.throw_exception(message.into());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn format_v8_exception(try_catch: &mut v8::PinnedRef<'_, v8::TryCatch<v8::HandleScope>>) -> String {
|
||||
let Some(exception) = try_catch.exception() else {
|
||||
return "JavaScript execution failed".to_string();
|
||||
};
|
||||
|
||||
if let Some(stack_trace) = try_catch.stack_trace()
|
||||
&& let Some(stack_trace) = stack_trace.to_string(try_catch)
|
||||
{
|
||||
let stack_trace = stack_trace.to_rust_string_lossy(try_catch);
|
||||
if !stack_trace.trim().is_empty() {
|
||||
return stack_trace;
|
||||
}
|
||||
}
|
||||
|
||||
let exception_string = exception
|
||||
.to_string(try_catch)
|
||||
.map(|value| value.to_rust_string_lossy(try_catch))
|
||||
.unwrap_or_else(|| "JavaScript execution failed".to_string());
|
||||
let Some(message) = try_catch.message() else {
|
||||
return exception_string;
|
||||
};
|
||||
|
||||
let filename = message
|
||||
.get_script_resource_name(try_catch)
|
||||
.and_then(|value| value.to_string(try_catch))
|
||||
.map(|value| value.to_rust_string_lossy(try_catch))
|
||||
.unwrap_or_else(|| "(unknown)".to_string());
|
||||
let line = message.get_line_number(try_catch).unwrap_or_default();
|
||||
format!("{filename}:{line}: {exception_string}")
|
||||
}
|
||||
|
||||
fn format_v8_value(scope: &mut v8::PinScope<'_, '_>, value: v8::Local<'_, v8::Value>) -> String {
|
||||
if value.is_object()
|
||||
&& let Ok(object) = v8::Local::<v8::Object>::try_from(value)
|
||||
&& let Some(stack_key) = v8::String::new(scope, "stack")
|
||||
&& let Some(stack_value) = object.get(scope, stack_key.into())
|
||||
&& let Some(stack_value) = stack_value.to_string(scope)
|
||||
{
|
||||
let stack_value = stack_value.to_rust_string_lossy(scope);
|
||||
if !stack_value.trim().is_empty() {
|
||||
return stack_value;
|
||||
}
|
||||
}
|
||||
|
||||
value
|
||||
.to_string(scope)
|
||||
.map(|value| value.to_rust_string_lossy(scope))
|
||||
.unwrap_or_else(|| "JavaScript execution failed".to_string())
|
||||
}
|
||||
|
||||
fn is_valid_identifier(name: &str) -> bool {
|
||||
let mut chars = name.chars();
|
||||
match chars.next() {
|
||||
Some(c) if c == '_' || c == '$' || c.is_ascii_alphabetic() => {}
|
||||
_ => return false,
|
||||
}
|
||||
chars.all(|c| c == '_' || c == '$' || c.is_ascii_alphanumeric())
|
||||
}
|
||||
|
||||
fn build_enabled_tools(exec: &ExecContext) -> Vec<EnabledTool> {
|
||||
|
||||
@@ -71,5 +71,3 @@ for (const name of __codexEnabledToolNames) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
__CODE_MODE_USER_CODE_PLACEHOLDER__
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const readline = require('node:readline');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const { SourceTextModule, SyntheticModule } = vm;
|
||||
|
||||
function createProtocol() {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
crlfDelay: Infinity,
|
||||
});
|
||||
|
||||
let nextId = 0;
|
||||
const pending = new Map();
|
||||
let initResolve;
|
||||
let initReject;
|
||||
const init = new Promise((resolve, reject) => {
|
||||
initResolve = resolve;
|
||||
initReject = reject;
|
||||
});
|
||||
|
||||
rl.on('line', (line) => {
|
||||
if (!line.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(line);
|
||||
} catch (error) {
|
||||
initReject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'init') {
|
||||
initResolve(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.type === 'response') {
|
||||
const entry = pending.get(message.id);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
pending.delete(message.id);
|
||||
entry.resolve(Array.isArray(message.content_items) ? message.content_items : []);
|
||||
return;
|
||||
}
|
||||
|
||||
initReject(new Error(`Unknown protocol message type: ${message.type}`));
|
||||
});
|
||||
|
||||
rl.on('close', () => {
|
||||
const error = new Error('stdin closed');
|
||||
initReject(error);
|
||||
for (const entry of pending.values()) {
|
||||
entry.reject(error);
|
||||
}
|
||||
pending.clear();
|
||||
});
|
||||
|
||||
function send(message) {
|
||||
return new Promise((resolve, reject) => {
|
||||
process.stdout.write(`${JSON.stringify(message)}\n`, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function request(type, payload) {
|
||||
const id = `msg-${++nextId}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
pending.set(id, { resolve, reject });
|
||||
void send({ type, id, ...payload }).catch((error) => {
|
||||
pending.delete(id);
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return { init, request, send };
|
||||
}
|
||||
|
||||
function readContentItems(context) {
|
||||
try {
|
||||
const serialized = vm.runInContext('JSON.stringify(globalThis.__codexContentItems ?? [])', context);
|
||||
const contentItems = JSON.parse(serialized);
|
||||
return Array.isArray(contentItems) ? contentItems : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function isValidIdentifier(name) {
|
||||
return /^[A-Za-z_$][0-9A-Za-z_$]*$/.test(name);
|
||||
}
|
||||
|
||||
function createToolsNamespace(protocol, enabledTools) {
|
||||
const tools = Object.create(null);
|
||||
|
||||
for (const { name } of enabledTools) {
|
||||
const callTool = async (args) =>
|
||||
protocol.request('tool_call', {
|
||||
name: String(name),
|
||||
input: args,
|
||||
});
|
||||
Object.defineProperty(tools, name, {
|
||||
value: callTool,
|
||||
configurable: false,
|
||||
enumerable: true,
|
||||
writable: false,
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze(tools);
|
||||
}
|
||||
|
||||
function createToolsModule(context, protocol, enabledTools) {
|
||||
const tools = createToolsNamespace(protocol, enabledTools);
|
||||
const exportNames = ['tools'];
|
||||
|
||||
for (const { name } of enabledTools) {
|
||||
if (name !== 'tools' && isValidIdentifier(name)) {
|
||||
exportNames.push(name);
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueExportNames = [...new Set(exportNames)];
|
||||
|
||||
return new SyntheticModule(
|
||||
uniqueExportNames,
|
||||
function initToolsModule() {
|
||||
this.setExport('tools', tools);
|
||||
for (const exportName of uniqueExportNames) {
|
||||
if (exportName !== 'tools') {
|
||||
this.setExport(exportName, tools[exportName]);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ context }
|
||||
);
|
||||
}
|
||||
|
||||
async function runModule(context, protocol, request) {
|
||||
const toolsModule = createToolsModule(context, protocol, request.enabled_tools ?? []);
|
||||
const mainModule = new SourceTextModule(request.source, {
|
||||
context,
|
||||
identifier: 'code_mode_main.mjs',
|
||||
importModuleDynamically(specifier) {
|
||||
if (specifier === 'tools.js') {
|
||||
return toolsModule;
|
||||
}
|
||||
throw new Error(`Unsupported import in code_mode: ${specifier}`);
|
||||
},
|
||||
});
|
||||
|
||||
await mainModule.link(async (specifier) => {
|
||||
if (specifier === 'tools.js') {
|
||||
return toolsModule;
|
||||
}
|
||||
throw new Error(`Unsupported import in code_mode: ${specifier}`);
|
||||
});
|
||||
await mainModule.evaluate();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const protocol = createProtocol();
|
||||
const request = await protocol.init;
|
||||
const context = vm.createContext({
|
||||
__codex_tool_call: async (name, input) =>
|
||||
protocol.request('tool_call', {
|
||||
name: String(name),
|
||||
input,
|
||||
}),
|
||||
});
|
||||
|
||||
try {
|
||||
await runModule(context, protocol, request);
|
||||
await protocol.send({
|
||||
type: 'result',
|
||||
content_items: readContentItems(context),
|
||||
});
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
process.stderr.write(`${String(error && error.stack ? error.stack : error)}\n`);
|
||||
await protocol.send({
|
||||
type: 'result',
|
||||
content_items: readContentItems(context),
|
||||
});
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
void main().catch(async (error) => {
|
||||
try {
|
||||
process.stderr.write(`${String(error && error.stack ? error.stack : error)}\n`);
|
||||
} finally {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
@@ -1549,7 +1549,7 @@ source: /[\s\S]+/
|
||||
enabled_tool_names.join(", ")
|
||||
};
|
||||
let description = format!(
|
||||
"Runs JavaScript in a Node-backed `node:vm` context. This is a freeform tool: send raw JavaScript source text (no JSON/quotes/markdown fences). Direct tool calls remain available while `code_mode` is enabled. Inside JavaScript, import nested tools from `tools.js`, for example `import {{ exec_command }} from \"tools.js\"` or `import {{ tools }} from \"tools.js\"`. `tools[name]` and identifier wrappers like `await shell(args)` remain available for compatibility when the tool name is a valid JS identifier. Nested tool calls resolve to arrays of content items. Function tools require JSON object arguments. Freeform tools require raw strings. Use synchronous `add_content(value)` with a content item or content-item array, including `add_content(await exec_command(...))`, to return the same content items a direct tool call would expose to the model. Only content passed to `add_content(value)` is surfaced back to the model. Enabled nested tools: {enabled_list}."
|
||||
"Runs JavaScript in an embedded V8 runtime. This is a freeform tool: send raw JavaScript source text (no JSON/quotes/markdown fences). Direct tool calls remain available while `code_mode` is enabled. Inside JavaScript, import nested tools from `tools.js`, for example `import {{ exec_command }} from \"tools.js\"` or `import {{ tools }} from \"tools.js\"`. `tools[name]` and identifier wrappers like `await shell(args)` remain available for compatibility when the tool name is a valid JS identifier. Nested tool calls resolve to arrays of content items. Function tools require JSON object arguments. Freeform tools require raw strings. Use synchronous `add_content(value)` with a content item or content-item array, including `add_content(await exec_command(...))`, to return the same content items a direct tool call would expose to the model. Only content passed to `add_content(value)` is surfaced back to the model. Enabled nested tools: {enabled_list}."
|
||||
);
|
||||
|
||||
ToolSpec::Freeform(FreeformTool {
|
||||
|
||||
@@ -16,6 +16,7 @@ use core_test_support::test_codex::test_codex;
|
||||
use pretty_assertions::assert_eq;
|
||||
use regex_lite::Regex;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use wiremock::MockServer;
|
||||
|
||||
fn custom_tool_output_text_and_success(
|
||||
@@ -134,3 +135,51 @@ async fn code_mode_can_apply_patch_via_nested_tool() -> Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn code_mode_does_not_require_node_startup_validation() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
let _ = config.features.enable(Feature::CodeMode);
|
||||
config.js_repl_node_path = Some(PathBuf::from("/definitely/missing/node"));
|
||||
});
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
responses::mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_custom_tool_call(
|
||||
"call-1",
|
||||
"code_mode",
|
||||
"add_content({ type: \"input_text\", text: \"v8-backed\" });",
|
||||
),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
let second_mock = responses::mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
|
||||
test.submit_turn("use code_mode without Node").await?;
|
||||
|
||||
let req = second_mock.single_request();
|
||||
let (output, success) = custom_tool_output_text_and_success(&req, "call-1");
|
||||
assert_ne!(
|
||||
success,
|
||||
Some(false),
|
||||
"code_mode call failed unexpectedly: {output}"
|
||||
);
|
||||
assert_eq!(output, "v8-backed");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user