From 58279bb6f6f7149472c9ee8ac34d73f110ad6ee2 Mon Sep 17 00:00:00 2001 From: Channing Conger Date: Thu, 12 Mar 2026 13:09:48 -0700 Subject: [PATCH 1/6] Move exec params from runtime declarations to @pragma --- codex-rs/core/src/tools/code_mode/bridge.js | 77 ++--- .../core/src/tools/code_mode/description.md | 16 +- .../src/tools/code_mode/execute_handler.rs | 113 ++++++- .../tools/code_mode/execute_handler_tests.rs | 41 +++ codex-rs/core/src/tools/code_mode/mod.rs | 2 +- codex-rs/core/src/tools/code_mode/protocol.rs | 3 +- codex-rs/core/src/tools/code_mode/runner.cjs | 189 +++--------- codex-rs/core/src/tools/spec.rs | 9 +- codex-rs/core/tests/suite/code_mode.rs | 285 ++++++++++++------ 9 files changed, 444 insertions(+), 291 deletions(-) create mode 100644 codex-rs/core/src/tools/code_mode/execute_handler_tests.rs diff --git a/codex-rs/core/src/tools/code_mode/bridge.js b/codex-rs/core/src/tools/code_mode/bridge.js index 5989985f1a9..d7967faabe7 100644 --- a/codex-rs/core/src/tools/code_mode/bridge.js +++ b/codex-rs/core/src/tools/code_mode/bridge.js @@ -1,9 +1,7 @@ +const __codexEnabledTools = __CODE_MODE_ENABLED_TOOLS_PLACEHOLDER__; const __codexContentItems = Array.isArray(globalThis.__codexContentItems) ? globalThis.__codexContentItems : []; -const __codexRuntime = globalThis.__codexRuntime; - -delete globalThis.__codexRuntime; Object.defineProperty(globalThis, '__codexContentItems', { value: __codexContentItems, @@ -13,42 +11,53 @@ Object.defineProperty(globalThis, '__codexContentItems', { }); (() => { - if (!__codexRuntime || typeof __codexRuntime !== 'object') { - throw new Error('code mode runtime is unavailable'); + function cloneContentItem(item) { + if (!item || typeof item !== 'object') { + throw new TypeError('content item must be an object'); + } + switch (item.type) { + case 'input_text': + if (typeof item.text !== 'string') { + throw new TypeError('content item "input_text" requires a string text field'); + } + return { type: 'input_text', text: item.text }; + case 'input_image': + if (typeof item.image_url !== 'string') { + throw new TypeError('content item "input_image" requires a string image_url field'); + } + return { type: 'input_image', image_url: item.image_url }; + default: + throw new TypeError(`unsupported content item type "${item.type}"`); + } + } + + function normalizeRawContentItems(value) { + if (Array.isArray(value)) { + return value.flatMap((entry) => normalizeRawContentItems(entry)); + } + return [cloneContentItem(value)]; } - function defineGlobal(name, value) { - Object.defineProperty(globalThis, name, { - value, - configurable: true, - enumerable: true, - writable: false, - }); + function normalizeContentItems(value) { + if (typeof value === 'string') { + return [{ type: 'input_text', text: value }]; + } + return normalizeRawContentItems(value); } - defineGlobal('ALL_TOOLS', __codexRuntime.ALL_TOOLS); - defineGlobal('image', __codexRuntime.image); - defineGlobal('load', __codexRuntime.load); - defineGlobal( - 'set_max_output_tokens_per_exec_call', - __codexRuntime.set_max_output_tokens_per_exec_call - ); - defineGlobal('set_yield_time', __codexRuntime.set_yield_time); - defineGlobal('store', __codexRuntime.store); - defineGlobal('text', __codexRuntime.text); - defineGlobal('tools', __codexRuntime.tools); - defineGlobal('yield_control', __codexRuntime.yield_control); + globalThis.add_content = (value) => { + const contentItems = normalizeContentItems(value); + __codexContentItems.push(...contentItems); + return contentItems; + }; - defineGlobal( - 'console', - Object.freeze({ - log() {}, - info() {}, - warn() {}, - error() {}, - debug() {}, - }) - ); + globalThis.console = Object.freeze({ + log() {}, + info() {}, + warn() {}, + error() {}, + debug() {}, + }); })(); __CODE_MODE_USER_CODE_PLACEHOLDER__ diff --git a/codex-rs/core/src/tools/code_mode/description.md b/codex-rs/core/src/tools/code_mode/description.md index d5e56454527..a611d69e3a0 100644 --- a/codex-rs/core/src/tools/code_mode/description.md +++ b/codex-rs/core/src/tools/code_mode/description.md @@ -1,16 +1,18 @@ ## exec - Runs raw JavaScript in an isolated context (no Node, no file system, or network access, no console). - Send raw JavaScript source text, not JSON, quoted strings, or markdown code fences. -- All nested tools are available on the global `tools` object, for example `await tools.exec_command(...)`. Tool names are exposed as normalized JavaScript identifiers, for example `await tools.mcp__ologs__get_profile(...)`. +- You may optionally start the tool input with a first-line pragma like `// @exec: {"yield_time_ms": 10000, "max_output_tokens": 1000}`. +- `yield_time_ms` asks `exec` to yield early after that many milliseconds if the script is still running. +- `max_output_tokens` sets the token budget for direct `exec` results. By default the result is truncated to 10000 tokens. +- You have a set of tools provided to you. They are imported either from `tools.js` or `/mcp/server.js` - Tool methods take either string or object as parameter. - They return either a structured value or a string based on the description above. -- Global helpers: -- `text(value: string | number | boolean | undefined | null)`: Appends a text item and returns it. Non-string values are stringified with `JSON.stringify(...)` when possible. -- `image(imageUrl: string)`: Appends an image item and returns it. `image_url` can be an HTTPS URL or a base64-encoded `data:` URL. +- Surface text back to the model with `output_text(v: string | number | boolean | undefined | null)`. A string representation of the value is returned to the model. Manually serialize complex values. + +- Methods available in `@openai/code_mode` module: +- `output_text(value: string | number | boolean | undefined | null)`: A string representation of the value is returned to the model. Manually serialize complex values. +- `output_image(imageUrl: string)`: An image is returned to the model. `image_url` can be an HTTPS URL or a base64-encoded `data:` URL. - `store(key: string, value: any)`: stores a serializeable value under a string key for later `exec` calls in the same session. - `load(key: string)`: returns the stored value for a string key, or `undefined` if it is missing. -- `ALL_TOOLS`: metadata for the enabled nested tools as `{ name, description }` entries. -- `set_max_output_tokens_per_exec_call(value)`: sets the token budget for direct `exec` results. By default the result is truncated to 10000 tokens. -- `set_yield_time(value)`: asks `exec` to yield early after that many milliseconds if the script is still running. - `yield_control()`: yields the accumulated output to the model immediately while the script keeps running. diff --git a/codex-rs/core/src/tools/code_mode/execute_handler.rs b/codex-rs/core/src/tools/code_mode/execute_handler.rs index 56a13ae47c2..6c9009948aa 100644 --- a/codex-rs/core/src/tools/code_mode/execute_handler.rs +++ b/codex-rs/core/src/tools/code_mode/execute_handler.rs @@ -1,4 +1,5 @@ use async_trait::async_trait; +use serde::Deserialize; use crate::codex::Session; use crate::codex::TurnContext; @@ -9,6 +10,7 @@ use crate::tools::context::ToolPayload; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolKind; +use super::CODE_MODE_PRAGMA_PREFIX; use super::CodeModeSessionProgress; use super::ExecContext; use super::PUBLIC_TOOL_NAME; @@ -18,6 +20,23 @@ use super::protocol::HostToNodeMessage; use super::protocol::build_source; pub struct CodeModeExecuteHandler; +const MAX_JS_SAFE_INTEGER: u64 = (1_u64 << 53) - 1; + +#[derive(Debug, Default, Deserialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +struct CodeModeExecPragma { + #[serde(default)] + yield_time_ms: Option, + #[serde(default)] + max_output_tokens: Option, +} + +#[derive(Debug, PartialEq, Eq)] +struct CodeModeExecArgs { + code: String, + yield_time_ms: Option, + max_output_tokens: Option, +} impl CodeModeExecuteHandler { async fn execute( @@ -26,12 +45,13 @@ impl CodeModeExecuteHandler { turn: std::sync::Arc, code: String, ) -> Result { + let args = parse_freeform_args(&code)?; let exec = ExecContext { session, turn }; let enabled_tools = build_enabled_tools(&exec).await; let service = &exec.session.services.code_mode_service; let stored_values = service.stored_values().await; let source = - build_source(&code, &enabled_tools).map_err(FunctionCallError::RespondToModel)?; + build_source(&args.code, &enabled_tools).map_err(FunctionCallError::RespondToModel)?; let cell_id = service.allocate_cell_id().await; let request_id = service.allocate_request_id().await; let process_slot = service @@ -46,6 +66,8 @@ impl CodeModeExecuteHandler { enabled_tools, stored_values, source, + yield_time_ms: args.yield_time_ms, + max_output_tokens: args.max_output_tokens, }; let result = { let mut process_slot = process_slot; @@ -72,6 +94,91 @@ impl CodeModeExecuteHandler { } } +fn parse_freeform_args(input: &str) -> Result { + if input.trim().is_empty() { + return Err(FunctionCallError::RespondToModel( + "exec expects raw JavaScript source text (non-empty). Provide JS only, optionally with first-line `// @exec: {\"yield_time_ms\": 10000, \"max_output_tokens\": 1000}`.".to_string(), + )); + } + + let mut args = CodeModeExecArgs { + code: input.to_string(), + yield_time_ms: None, + max_output_tokens: None, + }; + + let mut lines = input.splitn(2, '\n'); + let first_line = lines.next().unwrap_or_default(); + let rest = lines.next().unwrap_or_default(); + let trimmed = first_line.trim_start(); + let Some(pragma) = trimmed.strip_prefix(CODE_MODE_PRAGMA_PREFIX) else { + return Ok(args); + }; + + if rest.trim().is_empty() { + return Err(FunctionCallError::RespondToModel( + "exec pragma must be followed by JavaScript source on subsequent lines".to_string(), + )); + } + + let directive = pragma.trim(); + if directive.is_empty() { + return Err(FunctionCallError::RespondToModel( + "exec pragma must be a JSON object with supported fields `yield_time_ms` and `max_output_tokens`" + .to_string(), + )); + } + + let value: serde_json::Value = serde_json::from_str(directive).map_err(|err| { + FunctionCallError::RespondToModel(format!( + "exec pragma must be valid JSON with supported fields `yield_time_ms` and `max_output_tokens`: {err}" + )) + })?; + let object = value.as_object().ok_or_else(|| { + FunctionCallError::RespondToModel( + "exec pragma must be a JSON object with supported fields `yield_time_ms` and `max_output_tokens`" + .to_string(), + ) + })?; + for key in object.keys() { + match key.as_str() { + "yield_time_ms" | "max_output_tokens" => {} + _ => { + return Err(FunctionCallError::RespondToModel(format!( + "exec pragma only supports `yield_time_ms` and `max_output_tokens`; got `{key}`" + ))); + } + } + } + + let pragma: CodeModeExecPragma = serde_json::from_value(value).map_err(|err| { + FunctionCallError::RespondToModel(format!( + "exec pragma fields `yield_time_ms` and `max_output_tokens` must be non-negative safe integers: {err}" + )) + })?; + if pragma + .yield_time_ms + .is_some_and(|yield_time_ms| yield_time_ms > MAX_JS_SAFE_INTEGER) + { + return Err(FunctionCallError::RespondToModel( + "exec pragma field `yield_time_ms` must be a non-negative safe integer".to_string(), + )); + } + if pragma.max_output_tokens.is_some_and(|max_output_tokens| { + u64::try_from(max_output_tokens) + .map(|max_output_tokens| max_output_tokens > MAX_JS_SAFE_INTEGER) + .unwrap_or(true) + }) { + return Err(FunctionCallError::RespondToModel( + "exec pragma field `max_output_tokens` must be a non-negative safe integer".to_string(), + )); + } + args.code = rest.to_string(); + args.yield_time_ms = pragma.yield_time_ms; + args.max_output_tokens = pragma.max_output_tokens; + Ok(args) +} + #[async_trait] impl ToolHandler for CodeModeExecuteHandler { type Output = FunctionToolOutput; @@ -103,3 +210,7 @@ impl ToolHandler for CodeModeExecuteHandler { } } } + +#[cfg(test)] +#[path = "execute_handler_tests.rs"] +mod execute_handler_tests; diff --git a/codex-rs/core/src/tools/code_mode/execute_handler_tests.rs b/codex-rs/core/src/tools/code_mode/execute_handler_tests.rs new file mode 100644 index 00000000000..ed22b337b2d --- /dev/null +++ b/codex-rs/core/src/tools/code_mode/execute_handler_tests.rs @@ -0,0 +1,41 @@ +use super::parse_freeform_args; +use pretty_assertions::assert_eq; + +#[test] +fn parse_freeform_args_without_pragma() { + let args = parse_freeform_args("output_text('ok');").expect("parse args"); + assert_eq!(args.code, "output_text('ok');"); + assert_eq!(args.yield_time_ms, None); + assert_eq!(args.max_output_tokens, None); +} + +#[test] +fn parse_freeform_args_with_pragma() { + let input = concat!( + "// @exec: {\"yield_time_ms\": 15000, \"max_output_tokens\": 2000}\n", + "output_text('ok');", + ); + let args = parse_freeform_args(input).expect("parse args"); + assert_eq!(args.code, "output_text('ok');"); + assert_eq!(args.yield_time_ms, Some(15_000)); + assert_eq!(args.max_output_tokens, Some(2_000)); +} + +#[test] +fn parse_freeform_args_rejects_unknown_key() { + let err = parse_freeform_args("// @exec: {\"nope\": 1}\noutput_text('ok');") + .expect_err("expected error"); + assert_eq!( + err.to_string(), + "exec pragma only supports `yield_time_ms` and `max_output_tokens`; got `nope`" + ); +} + +#[test] +fn parse_freeform_args_rejects_missing_source() { + let err = parse_freeform_args("// @exec: {\"yield_time_ms\": 10}").expect_err("expected error"); + assert_eq!( + err.to_string(), + "exec pragma must be followed by JavaScript source on subsequent lines" + ); +} diff --git a/codex-rs/core/src/tools/code_mode/mod.rs b/codex-rs/core/src/tools/code_mode/mod.rs index ab25fd1ed31..9c18e992a31 100644 --- a/codex-rs/core/src/tools/code_mode/mod.rs +++ b/codex-rs/core/src/tools/code_mode/mod.rs @@ -33,6 +33,7 @@ const CODE_MODE_RUNNER_SOURCE: &str = include_str!("runner.cjs"); const CODE_MODE_BRIDGE_SOURCE: &str = include_str!("bridge.js"); const CODE_MODE_DESCRIPTION_TEMPLATE: &str = include_str!("description.md"); const CODE_MODE_WAIT_DESCRIPTION_TEMPLATE: &str = include_str!("wait_description.md"); +const CODE_MODE_PRAGMA_PREFIX: &str = "// @exec:"; pub(crate) const PUBLIC_TOOL_NAME: &str = "exec"; pub(crate) const WAIT_TOOL_NAME: &str = "exec_wait"; @@ -234,7 +235,6 @@ fn enabled_tool_from_spec(spec: ToolSpec) -> Option { }; Some(protocol::EnabledTool { - global_name: normalize_code_mode_identifier(&tool_name), tool_name, module_path: reference.module_path, namespace: reference.namespace, diff --git a/codex-rs/core/src/tools/code_mode/protocol.rs b/codex-rs/core/src/tools/code_mode/protocol.rs index 44757f858dc..81ff8fada06 100644 --- a/codex-rs/core/src/tools/code_mode/protocol.rs +++ b/codex-rs/core/src/tools/code_mode/protocol.rs @@ -17,7 +17,6 @@ pub(super) enum CodeModeToolKind { #[derive(Clone, Debug, Serialize)] pub(super) struct EnabledTool { pub(super) tool_name: String, - pub(super) global_name: String, #[serde(rename = "module")] pub(super) module_path: String, pub(super) namespace: Vec, @@ -46,6 +45,8 @@ pub(super) enum HostToNodeMessage { enabled_tools: Vec, stored_values: HashMap, source: String, + yield_time_ms: Option, + max_output_tokens: Option, }, Poll { request_id: String, diff --git a/codex-rs/core/src/tools/code_mode/runner.cjs b/codex-rs/core/src/tools/code_mode/runner.cjs index b498650a854..77053c71ee4 100644 --- a/codex-rs/core/src/tools/code_mode/runner.cjs +++ b/codex-rs/core/src/tools/code_mode/runner.cjs @@ -47,22 +47,6 @@ function codeModeWorkerMain() { const vm = require('node:vm'); const { SourceTextModule, SyntheticModule } = vm; - const DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL = 10000; - - function normalizeMaxOutputTokensPerExecCall(value) { - if (!Number.isSafeInteger(value) || value < 0) { - throw new TypeError('max_output_tokens_per_exec_call must be a non-negative safe integer'); - } - return value; - } - - function normalizeYieldTime(value) { - if (!Number.isSafeInteger(value) || value < 0) { - throw new TypeError('yield_time must be a non-negative safe integer'); - } - return value; - } - function formatErrorText(error) { return String(error && error.stack ? error.stack : error); } @@ -131,27 +115,17 @@ function codeModeWorkerMain() { return contentItems; } - function createGlobalToolsNamespace(callTool, enabledTools) { + function createToolsNamespace(callTool, enabledTools) { const tools = Object.create(null); - for (const { tool_name, global_name } of enabledTools) { - Object.defineProperty(tools, global_name, { - value: async (args) => callTool(tool_name, args), - configurable: false, - enumerable: true, - writable: false, - }); - } - - return Object.freeze(tools); - } - - function createModuleToolsNamespace(callTool, enabledTools) { - const tools = Object.create(null); + for (const tool of enabledTools) { + const toolNamespace = Array.isArray(tool.namespace) ? tool.namespace : []; + if (toolNamespace.length !== 0) { + continue; + } - for (const { tool_name, global_name } of enabledTools) { - Object.defineProperty(tools, global_name, { - value: async (args) => callTool(tool_name, args), + Object.defineProperty(tools, tool.name, { + value: async (args) => callTool(tool.tool_name, args), configurable: false, enumerable: true, writable: false, @@ -163,9 +137,10 @@ function codeModeWorkerMain() { function createAllToolsMetadata(enabledTools) { return Object.freeze( - enabledTools.map(({ global_name, description }) => + enabledTools.map(({ module: modulePath, name, description }) => Object.freeze({ - name: global_name, + module: modulePath, + name, description, }) ) @@ -173,16 +148,9 @@ function codeModeWorkerMain() { } function createToolsModule(context, callTool, enabledTools) { - const tools = createModuleToolsNamespace(callTool, enabledTools); + const tools = createToolsNamespace(callTool, enabledTools); const allTools = createAllToolsMetadata(enabledTools); - const exportNames = ['ALL_TOOLS']; - - for (const { global_name } of enabledTools) { - if (global_name !== 'ALL_TOOLS') { - exportNames.push(global_name); - } - } - + const exportNames = ['ALL_TOOLS', ...Object.keys(tools)]; const uniqueExportNames = [...new Set(exportNames)]; return new SyntheticModule( @@ -230,15 +198,15 @@ function codeModeWorkerMain() { function normalizeOutputImageUrl(value) { if (typeof value !== 'string' || !value) { - throw new TypeError('image expects a non-empty image URL string'); + throw new TypeError('output_image expects a non-empty image URL string'); } if (/^(?:https?:\/\/|data:)/i.test(value)) { return value; } - throw new TypeError('image expects an http(s) or data URL'); + throw new TypeError('output_image expects an http(s) or data URL'); } - function createCodeModeHelpers(context, state) { + function createCodeModeModule(context, state) { const load = (key) => { if (typeof key !== 'string') { throw new TypeError('load key must be a string'); @@ -254,7 +222,7 @@ function codeModeWorkerMain() { } state.storedValues[key] = cloneJsonValue(value); }; - const text = (value) => { + const outputText = (value) => { const item = { type: 'input_text', text: serializeOutputText(value), @@ -262,7 +230,7 @@ function codeModeWorkerMain() { ensureContentItems(context).push(item); return item; }; - const image = (value) => { + const outputImage = (value) => { const item = { type: 'input_image', image_url: normalizeOutputImageUrl(value), @@ -270,85 +238,21 @@ function codeModeWorkerMain() { ensureContentItems(context).push(item); return item; }; - const setMaxOutputTokensPerExecCall = (value) => { - const normalized = normalizeMaxOutputTokensPerExecCall(value); - state.maxOutputTokensPerExecCall = normalized; - parentPort.postMessage({ - type: 'set_max_output_tokens_per_exec_call', - value: normalized, - }); - return normalized; - }; - const setYieldTime = (value) => { - const normalized = normalizeYieldTime(value); - parentPort.postMessage({ - type: 'set_yield_time', - value: normalized, - }); - return normalized; - }; - const yieldControl = () => { - parentPort.postMessage({ type: 'yield' }); - }; - - return Object.freeze({ - image, - load, - output_image: image, - output_text: text, - set_max_output_tokens_per_exec_call: setMaxOutputTokensPerExecCall, - set_yield_time: setYieldTime, - store, - text, - yield_control: yieldControl, - }); - } - - function createCodeModeModule(context, helpers) { return new SyntheticModule( - [ - 'image', - 'load', - 'output_text', - 'output_image', - 'set_max_output_tokens_per_exec_call', - 'set_yield_time', - 'store', - 'text', - 'yield_control', - ], + ['load', 'output_text', 'output_image', 'store', 'yield_control'], function initCodeModeModule() { - this.setExport('image', helpers.image); - this.setExport('load', helpers.load); - this.setExport('output_text', helpers.output_text); - this.setExport('output_image', helpers.output_image); - this.setExport( - 'set_max_output_tokens_per_exec_call', - helpers.set_max_output_tokens_per_exec_call - ); - this.setExport('set_yield_time', helpers.set_yield_time); - this.setExport('store', helpers.store); - this.setExport('text', helpers.text); - this.setExport('yield_control', helpers.yield_control); + this.setExport('load', load); + this.setExport('output_text', outputText); + this.setExport('output_image', outputImage); + this.setExport('store', store); + this.setExport('yield_control', () => { + parentPort.postMessage({ type: 'yield' }); + }); }, { context } ); } - function createBridgeRuntime(callTool, enabledTools, helpers) { - return Object.freeze({ - ALL_TOOLS: createAllToolsMetadata(enabledTools), - image: helpers.image, - load: helpers.load, - set_max_output_tokens_per_exec_call: helpers.set_max_output_tokens_per_exec_call, - set_yield_time: helpers.set_yield_time, - store: helpers.store, - text: helpers.text, - tools: createGlobalToolsNamespace(callTool, enabledTools), - yield_control: helpers.yield_control, - }); - } - function namespacesMatch(left, right) { if (left.length !== right.length) { return false; @@ -399,7 +303,7 @@ function codeModeWorkerMain() { ); } - function createModuleResolver(context, callTool, enabledTools, helpers) { + function createModuleResolver(context, callTool, enabledTools, state) { let toolsModule; let codeModeModule; const namespacedModules = new Map(); @@ -410,7 +314,7 @@ function codeModeWorkerMain() { return toolsModule; } if (specifier === '@openai/code_mode' || specifier === 'openai/code_mode') { - codeModeModule ??= createCodeModeModule(context, helpers); + codeModeModule ??= createCodeModeModule(context, state); return codeModeModule; } const namespacedMatch = /^tools\/(.+)\.js$/.exec(specifier); @@ -454,12 +358,12 @@ function codeModeWorkerMain() { return module; } - async function runModule(context, start, callTool, helpers) { + async function runModule(context, start, callTool, state) { const resolveModule = createModuleResolver( context, callTool, start.enabled_tools ?? [], - helpers + state ); const mainModule = new SourceTextModule(start.source, { context, @@ -475,25 +379,16 @@ function codeModeWorkerMain() { async function main() { const start = workerData ?? {}; const state = { - maxOutputTokensPerExecCall: DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL, storedValues: cloneJsonValue(start.stored_values ?? {}), }; const callTool = createToolCaller(); - const enabledTools = start.enabled_tools ?? []; const contentItems = createContentItems(); const context = vm.createContext({ __codexContentItems: contentItems, }); - const helpers = createCodeModeHelpers(context, state); - Object.defineProperty(context, '__codexRuntime', { - value: createBridgeRuntime(callTool, enabledTools, helpers), - configurable: true, - enumerable: false, - writable: false, - }); try { - await runModule(context, start, callTool, helpers); + await runModule(context, start, callTool, state); parentPort.postMessage({ type: 'result', stored_values: state.storedValues, @@ -650,6 +545,10 @@ function sessionWorkerSource() { } function startSession(protocol, sessions, start) { + const maxOutputTokensPerExecCall = + start.max_output_tokens == null + ? DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL + : normalizeMaxOutputTokensPerExecCall(start.max_output_tokens); const session = { completed: false, content_items: [], @@ -657,7 +556,7 @@ function startSession(protocol, sessions, start) { id: start.cell_id, initial_yield_timer: null, initial_yield_triggered: false, - max_output_tokens_per_exec_call: DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL, + max_output_tokens_per_exec_call: maxOutputTokensPerExecCall, pending_result: null, poll_yield_timer: null, request_id: String(start.request_id), @@ -667,7 +566,11 @@ function startSession(protocol, sessions, start) { }), }; sessions.set(session.id, session); - scheduleInitialYield(protocol, session, session.default_yield_time_ms); + const initialYieldTime = + start.yield_time_ms == null + ? session.default_yield_time_ms + : normalizeYieldTime(start.yield_time_ms); + scheduleInitialYield(protocol, session, initialYieldTime); session.worker.on('message', (message) => { void handleWorkerMessage(protocol, sessions, session, message).catch((error) => { @@ -706,16 +609,6 @@ async function handleWorkerMessage(protocol, sessions, session, message) { return; } - if (message.type === 'set_yield_time') { - scheduleInitialYield(protocol, session, normalizeYieldTime(message.value ?? 0)); - return; - } - - if (message.type === 'set_max_output_tokens_per_exec_call') { - session.max_output_tokens_per_exec_call = normalizeMaxOutputTokensPerExecCall(message.value); - return; - } - if (message.type === 'yield') { void sendYielded(protocol, session); return; diff --git a/codex-rs/core/src/tools/spec.rs b/codex-rs/core/src/tools/spec.rs index 28f522847d3..6e679bbfd97 100644 --- a/codex-rs/core/src/tools/spec.rs +++ b/codex-rs/core/src/tools/spec.rs @@ -2018,8 +2018,13 @@ fn create_js_repl_reset_tool() -> ToolSpec { fn create_code_mode_tool(enabled_tool_names: &[String]) -> ToolSpec { const CODE_MODE_FREEFORM_GRAMMAR: &str = r#" -start: source -source: /[\s\S]+/ +start: pragma_source | plain_source +pragma_source: PRAGMA_LINE NEWLINE SOURCE +plain_source: SOURCE + +PRAGMA_LINE: /[ \t]*\/\/ @exec:[^\r\n]*/ +NEWLINE: /\r?\n/ +SOURCE: /[\s\S]+/ "#; ToolSpec::Freeform(FreeformTool { diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index c6fc3dea96d..aa59e5cd09b 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -63,7 +63,7 @@ fn wait_for_file_source(path: &Path) -> Result { let quoted_path = shlex::try_join([path.to_string_lossy().as_ref()])?; let command = format!("if [ -f {quoted_path} ]; then printf ready; fi"); Ok(format!( - r#"while ((await tools.exec_command({{ cmd: {command:?} }})).output !== "ready") {{ + r#"while ((await exec_command({{ cmd: {command:?} }})).output !== "ready") {{ }}"# )) } @@ -197,7 +197,9 @@ async fn code_mode_can_return_exec_command_output() -> Result<()> { &server, "use exec to run exec_command", r#" -text(JSON.stringify(await tools.exec_command({ cmd: "printf code_mode_exec_marker" }))); +import { exec_command } from "tools.js"; + +add_content(JSON.stringify(await exec_command({ cmd: "printf code_mode_exec_marker" }))); "#, false, ) @@ -245,6 +247,8 @@ async fn code_mode_nested_tool_calls_can_run_in_parallel() -> Result<()> { let test = builder.build(&server).await?; let warmup_code = r#" +import { test_sync_tool } from "tools.js"; + const args = { sleep_after_ms: 10, barrier: { @@ -255,11 +259,13 @@ const args = { }; await Promise.all([ - tools.test_sync_tool(args), - tools.test_sync_tool(args), + test_sync_tool(args), + test_sync_tool(args), ]); "#; let code = r#" +import { test_sync_tool } from "tools.js"; + const args = { sleep_after_ms: 300, barrier: { @@ -270,11 +276,11 @@ const args = { }; const results = await Promise.all([ - tools.test_sync_tool(args), - tools.test_sync_tool(args), + test_sync_tool(args), + test_sync_tool(args), ]); -text(JSON.stringify(results)); +add_content(JSON.stringify(results)); "#; let response_mock = responses::mount_sse_sequence( @@ -332,10 +338,10 @@ async fn code_mode_can_truncate_final_result_with_configured_budget() -> Result< let (_test, second_mock) = run_code_mode_turn( &server, "use exec to truncate the final result", - r#" -set_max_output_tokens_per_exec_call(6); + r#"// @exec: {"max_output_tokens": 6} +import { exec_command } from "tools.js"; -text(JSON.stringify(await tools.exec_command({ +add_content(JSON.stringify(await exec_command({ cmd: "printf 'token one token two token three token four token five token six token seven'", max_output_tokens: 100 }))); @@ -375,8 +381,8 @@ async fn code_mode_returns_accumulated_output_when_script_fails() -> Result<()> &server, "use code_mode to surface script failures", r#" -text("before crash"); -text("still before crash"); +add_content("before crash"); +add_content("still before crash"); throw new Error("boom"); "#, false, @@ -425,13 +431,15 @@ async fn code_mode_can_yield_and_resume_with_exec_wait() -> Result<()> { let phase_3_wait = wait_for_file_source(&phase_3_gate)?; let code = format!( - r#" -text("phase 1"); -set_yield_time(10); + r#"// @exec: {{"yield_time_ms": 10}} +import {{ output_text }} from "@openai/code_mode"; +import {{ exec_command }} from "tools.js"; + +output_text("phase 1"); {phase_2_wait} -text("phase 2"); +output_text("phase 2"); {phase_3_wait} -text("phase 3"); +output_text("phase 3"); "# ); @@ -566,9 +574,10 @@ async fn code_mode_yield_timeout_works_for_busy_loop() -> Result<()> { }); let test = builder.build(&server).await?; - let code = r#" -text("phase 1"); -set_yield_time(10); + let code = r#"// @exec: {"yield_time_ms": 10} +import { output_text } from "@openai/code_mode"; + +output_text("phase 1"); while (true) {} "#; @@ -666,19 +675,23 @@ async fn code_mode_can_run_multiple_yielded_sessions() -> Result<()> { let session_b_wait = wait_for_file_source(&session_b_gate)?; let session_a_code = format!( - r#" -text("session a start"); -set_yield_time(10); + r#"// @exec: {{"yield_time_ms": 10}} +import {{ output_text }} from "@openai/code_mode"; +import {{ exec_command }} from "tools.js"; + +output_text("session a start"); {session_a_wait} -text("session a done"); +output_text("session a done"); "# ); let session_b_code = format!( - r#" -text("session b start"); -set_yield_time(10); + r#"// @exec: {{"yield_time_ms": 10}} +import {{ output_text }} from "@openai/code_mode"; +import {{ exec_command }} from "tools.js"; + +output_text("session b start"); {session_b_wait} -text("session b done"); +output_text("session b done"); "# ); @@ -832,11 +845,13 @@ async fn code_mode_exec_wait_can_terminate_and_continue() -> Result<()> { let termination_wait = wait_for_file_source(&termination_gate)?; let code = format!( - r#" -text("phase 1"); -set_yield_time(10); + r#"// @exec: {{"yield_time_ms": 10}} +import {{ output_text }} from "@openai/code_mode"; +import {{ exec_command }} from "tools.js"; + +output_text("phase 1"); {termination_wait} -text("phase 2"); +output_text("phase 2"); "# ); @@ -912,7 +927,9 @@ text("phase 2"); "call-3", "exec", r#" -text("after terminate"); +import { output_text } from "@openai/code_mode"; + +output_text("after terminate"); "#, ), ev_completed("resp-5"), @@ -1026,20 +1043,24 @@ async fn code_mode_exec_wait_terminate_returns_completed_session_if_it_finished_ let session_a_done_command = format!("printf done > {session_a_done_marker_quoted}"); let session_a_code = format!( - r#" -text("session a start"); -set_yield_time(10); + r#"// @exec: {{"yield_time_ms": 10}} +import {{ output_text }} from "@openai/code_mode"; +import {{ exec_command }} from "tools.js"; + +output_text("session a start"); {session_a_wait} -text("session a done"); -await tools.exec_command({{ cmd: {session_a_done_command:?} }}); +output_text("session a done"); +await exec_command({{ cmd: {session_a_done_command:?} }}); "# ); let session_b_code = format!( - r#" -text("session b start"); -set_yield_time(10); + r#"// @exec: {{"yield_time_ms": 10}} +import {{ output_text }} from "@openai/code_mode"; +import {{ exec_command }} from "tools.js"; + +output_text("session b start"); {session_b_wait} -text("session b done"); +output_text("session b done"); "# ); @@ -1218,10 +1239,13 @@ async fn code_mode_background_keeps_running_on_later_turn_without_exec_wait() -> format!("while [ ! -f {resumed_file_quoted} ]; do sleep 0.01; done; printf ready"); let code = format!( r#" -text("before yield"); +import {{ yield_control, output_text }} from "@openai/code_mode"; +import {{ exec_command }} from "tools.js"; + +output_text("before yield"); yield_control(); -await tools.exec_command({{ cmd: {write_file_command:?} }}); -text("after yield"); +await exec_command({{ cmd: {write_file_command:?} }}); +output_text("after yield"); "# ); @@ -1308,12 +1332,13 @@ async fn code_mode_exec_wait_uses_its_own_max_tokens_budget() -> Result<()> { let completion_wait = wait_for_file_source(&completion_gate)?; let code = format!( - r#" -text("phase 1"); -set_max_output_tokens_per_exec_call(100); -set_yield_time(10); + r#"// @exec: {{"max_output_tokens": 100, "yield_time_ms": 10}} +import {{ output_text }} from "@openai/code_mode"; +import {{ exec_command }} from "tools.js"; + +output_text("phase 1"); {completion_wait} -text("token one token two token three token four token five token six token seven"); +output_text("token one token two token three token four token five token six token seven"); "# ); @@ -1395,7 +1420,7 @@ Total\ output\ lines:\ 1\n } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn code_mode_can_output_serialized_text_via_global_helper() -> Result<()> { +async fn code_mode_can_output_serialized_text_via_openai_code_mode_module() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1403,7 +1428,9 @@ async fn code_mode_can_output_serialized_text_via_global_helper() -> Result<()> &server, "use exec to return structured text", r#" -text({ json: true }); +import { output_text } from "@openai/code_mode"; + +output_text({ json: true }); "#, false, ) @@ -1422,7 +1449,7 @@ text({ json: true }); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn code_mode_surfaces_text_stringify_errors() -> Result<()> { +async fn code_mode_surfaces_output_text_stringify_errors() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1430,9 +1457,11 @@ async fn code_mode_surfaces_text_stringify_errors() -> Result<()> { &server, "use exec to return circular text", r#" +import { output_text } from "@openai/code_mode"; + const circular = {}; circular.self = circular; -text(circular); +output_text(circular); "#, false, ) @@ -1463,7 +1492,7 @@ text(circular); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn code_mode_can_output_images_via_global_helper() -> Result<()> { +async fn code_mode_can_output_images_via_openai_code_mode_module() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1471,8 +1500,10 @@ async fn code_mode_can_output_images_via_global_helper() -> Result<()> { &server, "use exec to return images", r#" -image("https://example.com/image.jpg"); -image("data:image/png;base64,AAA"); +import { output_image } from "@openai/code_mode"; + +output_image("https://example.com/image.jpg"); +output_image("data:image/png;base64,AAA"); "#, false, ) @@ -1521,7 +1552,9 @@ async fn code_mode_can_apply_patch_via_nested_tool() -> Result<()> { let patch = format!( "*** Begin Patch\n*** Add File: {file_name}\n+hello from code_mode\n*** End Patch\n" ); - let code = format!("text(await tools.apply_patch({patch:?}));\n"); + let code = format!( + "import {{ apply_patch }} from \"tools.js\";\nconst items = await apply_patch({patch:?});\nadd_content(items);\n" + ); let (test, second_mock) = run_code_mode_turn(&server, "use exec to run apply_patch", &code, true).await?; @@ -1557,10 +1590,12 @@ async fn code_mode_can_print_structured_mcp_tool_result_fields() -> Result<()> { let server = responses::start_mock_server().await; let code = r#" -const { content, structuredContent, isError } = await tools.mcp__rmcp__echo({ +import { echo } from "tools/mcp/rmcp.js"; + +const { content, structuredContent, isError } = await echo({ message: "ping", }); -text( +add_content( `echo=${structuredContent?.echo ?? "missing"}\n` + `env=${structuredContent?.env ?? "missing"}\n` + `isError=${String(isError)}\n` + @@ -1590,33 +1625,37 @@ contentLength=0" } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn code_mode_exposes_mcp_tools_on_global_tools_object() -> Result<()> { +async fn code_mode_can_dynamically_import_namespaced_mcp_tools() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; let code = r#" -const { content, structuredContent, isError } = await tools.mcp__rmcp__echo({ +const rmcp = await import("tools/mcp/rmcp.js"); +const { content, structuredContent, isError } = await rmcp.echo({ message: "ping", }); -text( - `hasEcho=${String(Object.keys(tools).includes("mcp__rmcp__echo"))}\n` + - `echoType=${typeof tools.mcp__rmcp__echo}\n` + +add_content( + `hasEcho=${String(Object.keys(rmcp).includes("echo"))}\n` + + `echoType=${typeof rmcp.echo}\n` + `echo=${structuredContent?.echo ?? "missing"}\n` + `isError=${String(isError)}\n` + `contentLength=${content.length}` ); "#; - let (_test, second_mock) = - run_code_mode_turn_with_rmcp(&server, "use exec to inspect the global tools object", code) - .await?; + let (_test, second_mock) = run_code_mode_turn_with_rmcp( + &server, + "use exec to dynamically import the rmcp module", + code, + ) + .await?; let req = second_mock.single_request(); let (output, success) = custom_tool_output_body_and_success(&req, "call-1"); assert_ne!( success, Some(false), - "exec global rmcp access failed unexpectedly: {output}" + "exec dynamic rmcp import failed unexpectedly: {output}" ); assert_eq!( output, @@ -1631,18 +1670,63 @@ contentLength=0" } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn code_mode_exposes_normalized_illegal_mcp_tool_names() -> Result<()> { +async fn code_mode_does_not_export_namespaced_mcp_tools_from_tools_js() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let code = r#" +const tools = await import("tools.js"); +const exports = Object.keys(tools).sort(); +add_content(JSON.stringify({ + hasAllTools: exports.includes("ALL_TOOLS"), + hasExecCommand: exports.includes("exec_command"), + namespacedExports: exports.filter((name) => name.startsWith("mcp__")), +})); +"#; + + let (_test, second_mock) = run_code_mode_turn_with_rmcp( + &server, + "use exec to inspect the top-level tools module", + code, + ) + .await?; + + let req = second_mock.single_request(); + let (output, success) = custom_tool_output_body_and_success(&req, "call-1"); + assert_ne!( + success, + Some(false), + "exec tools.js inspection failed unexpectedly: {output}" + ); + + let parsed: Value = serde_json::from_str(&output)?; + assert_eq!( + parsed, + serde_json::json!({ + "hasAllTools": true, + "hasExecCommand": true, + "namespacedExports": [], + }) + ); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn code_mode_normalizes_illegal_namespaced_mcp_tool_identifiers() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; let code = r#" -const result = await tools.mcp__rmcp__echo_tool({ message: "ping" }); -text(`echo=${result.structuredContent.echo}`); +import { echo_tool } from "tools/mcp/rmcp.js"; + +const result = await echo_tool({ message: "ping" }); +add_content(`echo=${result.structuredContent.echo}`); "#; let (_test, second_mock) = run_code_mode_turn_with_rmcp( &server, - "use exec to call a normalized rmcp tool name", + "use exec to import a normalized rmcp tool name", code, ) .await?; @@ -1652,7 +1736,7 @@ text(`echo=${result.structuredContent.echo}`); assert_ne!( success, Some(false), - "exec normalized rmcp tool call failed unexpectedly: {output}" + "exec normalized rmcp import failed unexpectedly: {output}" ); assert_eq!(output, "echo=ECHOING: ping"); @@ -1665,7 +1749,7 @@ async fn code_mode_lists_global_scope_items() -> Result<()> { let server = responses::start_mock_server().await; let code = r#" -text(JSON.stringify(Object.getOwnPropertyNames(globalThis).sort())); +add_content(JSON.stringify(Object.getOwnPropertyNames(globalThis).sort())); "#; let (_test, second_mock) = @@ -1682,7 +1766,6 @@ text(JSON.stringify(Object.getOwnPropertyNames(globalThis).sort())); let globals = globals.into_iter().collect::>(); let expected = [ "AggregateError", - "ALL_TOOLS", "Array", "ArrayBuffer", "AsyncDisposableStack", @@ -1736,6 +1819,7 @@ text(JSON.stringify(Object.getOwnPropertyNames(globalThis).sort())); "WeakSet", "WebAssembly", "__codexContentItems", + "add_content", "console", "decodeURI", "decodeURIComponent", @@ -1744,20 +1828,12 @@ text(JSON.stringify(Object.getOwnPropertyNames(globalThis).sort())); "escape", "eval", "globalThis", - "image", "isFinite", "isNaN", - "load", "parseFloat", "parseInt", - "set_max_output_tokens_per_exec_call", - "set_yield_time", - "store", - "text", - "tools", "undefined", "unescape", - "yield_control", ]; for g in &globals { assert!( @@ -1775,8 +1851,10 @@ async fn code_mode_exports_all_tools_metadata_for_builtin_tools() -> Result<()> let server = responses::start_mock_server().await; let code = r#" -const tool = ALL_TOOLS.find(({ name }) => name === "view_image"); -text(JSON.stringify(tool)); +import { ALL_TOOLS } from "tools.js"; + +const tool = ALL_TOOLS.find(({ module, name }) => module === "tools.js" && name === "view_image"); +add_content(JSON.stringify(tool)); "#; let (_test, second_mock) = @@ -1794,8 +1872,9 @@ text(JSON.stringify(tool)); assert_eq!( parsed, serde_json::json!({ + "module": "tools.js", "name": "view_image", - "description": "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags).\n\nCode mode declaration:\n```ts\ndeclare const tools: {\n view_image(args: {\n path: string;\n }): Promise;\n};\n```", + "description": "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags).\n\nCode mode declaration:\n```ts\nimport { view_image } from \"tools.js\";\ndeclare function view_image(args: {\n path: string;\n}): Promise;\n```", }) ); @@ -1808,10 +1887,12 @@ async fn code_mode_exports_all_tools_metadata_for_namespaced_mcp_tools() -> Resu let server = responses::start_mock_server().await; let code = r#" +import { ALL_TOOLS } from "tools.js"; + const tool = ALL_TOOLS.find( - ({ name }) => name === "mcp__rmcp__echo" + ({ module, name }) => module === "tools/mcp/rmcp.js" && name === "echo" ); -text(JSON.stringify(tool)); +add_content(JSON.stringify(tool)); "#; let (_test, second_mock) = @@ -1829,8 +1910,9 @@ text(JSON.stringify(tool)); assert_eq!( parsed, serde_json::json!({ - "name": "mcp__rmcp__echo", - "description": "Echo back the provided message and include environment data.\n\nCode mode declaration:\n```ts\ndeclare const tools: {\n mcp__rmcp__echo(args: {\n env_var?: string;\n message: string;\n }): Promise<{\n _meta?: unknown;\n content: Array;\n isError?: boolean;\n structuredContent?: unknown;\n }>;\n};\n```", + "module": "tools/mcp/rmcp.js", + "name": "echo", + "description": "Echo back the provided message and include environment data.\n\nCode mode declaration:\n```ts\nimport { echo } from \"tools/mcp/rmcp.js\";\ndeclare function echo(args: {\n env_var?: string;\n message: string;\n}): Promise<{\n _meta?: unknown;\n content: Array;\n isError?: boolean;\n structuredContent?: unknown;\n}>;\n```", }) ); @@ -1843,11 +1925,13 @@ async fn code_mode_can_print_content_only_mcp_tool_result_fields() -> Result<()> let server = responses::start_mock_server().await; let code = r#" -const { content, structuredContent, isError } = await tools.mcp__rmcp__image_scenario({ +import { image_scenario } from "tools/mcp/rmcp.js"; + +const { content, structuredContent, isError } = await image_scenario({ scenario: "text_only", caption: "caption from mcp", }); -text( +add_content( `firstType=${content[0]?.type ?? "missing"}\n` + `firstText=${content[0]?.text ?? "missing"}\n` + `structuredContent=${String(structuredContent ?? null)}\n` + @@ -1886,11 +1970,13 @@ async fn code_mode_can_print_error_mcp_tool_result_fields() -> Result<()> { let server = responses::start_mock_server().await; let code = r#" -const { content, structuredContent, isError } = await tools.mcp__rmcp__echo({}); +import { echo } from "tools/mcp/rmcp.js"; + +const { content, structuredContent, isError } = await echo({}); const firstText = content[0]?.text ?? ""; const mentionsMissingMessage = firstText.includes("missing field") && firstText.includes("message"); -text( +add_content( `isError=${String(isError)}\n` + `contentLength=${content.length}\n` + `mentionsMissingMessage=${String(mentionsMissingMessage)}\n` + @@ -1918,6 +2004,7 @@ structuredContent=null" Ok(()) } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn code_mode_can_store_and_load_values_across_turns() -> Result<()> { skip_if_no_network!(Ok(())); @@ -1936,8 +2023,10 @@ async fn code_mode_can_store_and_load_values_across_turns() -> Result<()> { "call-1", "exec", r#" +import { store } from "@openai/code_mode"; + store("nb", { title: "Notebook", items: [1, true, null] }); -text("stored"); +add_content("stored"); "#, ), ev_completed("resp-1"), @@ -1973,7 +2062,9 @@ text("stored"); "call-2", "exec", r#" -text(JSON.stringify(load("nb"))); +import { load } from "openai/code_mode"; + +add_content(JSON.stringify(load("nb"))); "#, ), ev_completed("resp-3"), From ceeed16205094d53756ad4dca12c0b5528aa2c8d Mon Sep 17 00:00:00 2001 From: Channing Conger Date: Thu, 12 Mar 2026 18:01:11 -0700 Subject: [PATCH 2/6] Actually fix the tests --- codex-rs/core/src/tools/code_mode_description.rs | 7 +++++-- codex-rs/core/src/tools/code_mode_description_tests.rs | 6 +++--- codex-rs/core/src/tools/spec_tests.rs | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/codex-rs/core/src/tools/code_mode_description.rs b/codex-rs/core/src/tools/code_mode_description.rs index c5657fcacea..aaf1d5c46ca 100644 --- a/codex-rs/core/src/tools/code_mode_description.rs +++ b/codex-rs/core/src/tools/code_mode_description.rs @@ -74,9 +74,12 @@ fn append_code_mode_sample( input_type: String, output_type: String, ) -> String { + let reference = code_mode_tool_reference(tool_name); + let import_name = normalize_code_mode_identifier(&reference.tool_key); let declaration = format!( - "declare const tools: {{\n {}\n}};", - render_code_mode_tool_declaration(tool_name, input_name, input_type, output_type) + "import {{ {import_name} }} from \"{}\";\ndeclare function {}", + reference.module_path, + render_code_mode_tool_declaration(&reference.tool_key, input_name, input_type, output_type) ); format!("{description}\n\nCode mode declaration:\n```ts\n{declaration}\n```") } diff --git a/codex-rs/core/src/tools/code_mode_description_tests.rs b/codex-rs/core/src/tools/code_mode_description_tests.rs index d014fc40984..8867e3cc27d 100644 --- a/codex-rs/core/src/tools/code_mode_description_tests.rs +++ b/codex-rs/core/src/tools/code_mode_description_tests.rs @@ -76,7 +76,7 @@ fn render_json_schema_to_typescript_sorts_object_properties() { } #[test] -fn append_code_mode_sample_uses_global_tools_for_valid_identifiers() { +fn append_code_mode_sample_uses_module_imports_for_valid_identifiers() { assert_eq!( append_code_mode_sample( "desc", @@ -85,7 +85,7 @@ fn append_code_mode_sample_uses_global_tools_for_valid_identifiers() { "{ foo: string }".to_string(), "unknown".to_string(), ), - "desc\n\nCode mode declaration:\n```ts\ndeclare const tools: {\n mcp__ologs__get_profile(args: { foo: string }): Promise;\n};\n```" + "desc\n\nCode mode declaration:\n```ts\nimport { get_profile } from \"tools/mcp/ologs.js\";\ndeclare function get_profile(args: { foo: string }): Promise;\n```" ); } @@ -99,6 +99,6 @@ fn append_code_mode_sample_normalizes_invalid_identifiers() { "{ foo: string }".to_string(), "unknown".to_string(), ), - "desc\n\nCode mode declaration:\n```ts\ndeclare const tools: {\n mcp__rmcp__echo_tool(args: { foo: string }): Promise;\n};\n```" + "desc\n\nCode mode declaration:\n```ts\nimport { echo_tool } from \"tools/mcp/rmcp.js\";\ndeclare function echo_tool(args: { foo: string }): Promise;\n```" ); } diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 30147ffc90d..07352aafa00 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -2446,7 +2446,7 @@ fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() { assert_eq!( description, - "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags).\n\nCode mode declaration:\n```ts\ndeclare const tools: {\n view_image(args: {\n path: string;\n }): Promise;\n};\n```" + "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags).\n\nCode mode declaration:\n```ts\nimport { view_image } from \"tools.js\";\ndeclare function view_image(args: {\n path: string;\n }): Promise;\n```" ); } @@ -2498,7 +2498,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() { assert_eq!( description, - "Echo text\n\nCode mode declaration:\n```ts\ndeclare const tools: {\n mcp__sample__echo(args: {\n message: string;\n }): Promise<{\n _meta?: unknown;\n content: Array;\n isError?: boolean;\n structuredContent?: unknown;\n }>;\n};\n```" + "Echo text\n\nCode mode declaration:\n```ts\nimport { echo } from \"tools/mcp/sample.js\";\ndeclare function echo(args: {\n message: string;\n }): Promise<{\n _meta?: unknown;\n content: Array;\n isError?: boolean;\n structuredContent?: unknown;\n }>;\n```" ); } From f9851edf13b5700d4fc8483a1ae4f0189dfe4493 Mon Sep 17 00:00:00 2001 From: Channing Conger Date: Thu, 12 Mar 2026 18:22:06 -0700 Subject: [PATCH 3/6] more test regressions --- codex-rs/core/tests/suite/code_mode.rs | 92 +++++++++++++++++--------- 1 file changed, 59 insertions(+), 33 deletions(-) diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index aa59e5cd09b..8f473a93e9b 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -28,11 +28,13 @@ use std::time::Instant; use wiremock::MockServer; fn custom_tool_output_items(req: &ResponsesRequest, call_id: &str) -> Vec { - req.custom_tool_call_output(call_id) - .get("output") - .and_then(Value::as_array) - .expect("custom tool output should be serialized as content items") - .clone() + match req.custom_tool_call_output(call_id).get("output") { + Some(Value::Array(items)) => items.clone(), + Some(Value::String(text)) => { + vec![serde_json::json!({ "type": "input_text", "text": text })] + } + _ => panic!("custom tool output should be serialized as text or content items"), + } } fn function_tool_output_items(req: &ResponsesRequest, call_id: &str) -> Vec { @@ -431,11 +433,12 @@ async fn code_mode_can_yield_and_resume_with_exec_wait() -> Result<()> { let phase_3_wait = wait_for_file_source(&phase_3_gate)?; let code = format!( - r#"// @exec: {{"yield_time_ms": 10}} -import {{ output_text }} from "@openai/code_mode"; + r#" +import {{ output_text, yield_control }} from "@openai/code_mode"; import {{ exec_command }} from "tools.js"; output_text("phase 1"); +yield_control(); {phase_2_wait} output_text("phase 2"); {phase_3_wait} @@ -574,7 +577,7 @@ async fn code_mode_yield_timeout_works_for_busy_loop() -> Result<()> { }); let test = builder.build(&server).await?; - let code = r#"// @exec: {"yield_time_ms": 10} + let code = r#"// @exec: {"yield_time_ms": 100} import { output_text } from "@openai/code_mode"; output_text("phase 1"); @@ -675,21 +678,23 @@ async fn code_mode_can_run_multiple_yielded_sessions() -> Result<()> { let session_b_wait = wait_for_file_source(&session_b_gate)?; let session_a_code = format!( - r#"// @exec: {{"yield_time_ms": 10}} -import {{ output_text }} from "@openai/code_mode"; + r#" +import {{ output_text, yield_control }} from "@openai/code_mode"; import {{ exec_command }} from "tools.js"; output_text("session a start"); +yield_control(); {session_a_wait} output_text("session a done"); "# ); let session_b_code = format!( - r#"// @exec: {{"yield_time_ms": 10}} -import {{ output_text }} from "@openai/code_mode"; + r#" +import {{ output_text, yield_control }} from "@openai/code_mode"; import {{ exec_command }} from "tools.js"; output_text("session b start"); +yield_control(); {session_b_wait} output_text("session b done"); "# @@ -845,11 +850,12 @@ async fn code_mode_exec_wait_can_terminate_and_continue() -> Result<()> { let termination_wait = wait_for_file_source(&termination_gate)?; let code = format!( - r#"// @exec: {{"yield_time_ms": 10}} -import {{ output_text }} from "@openai/code_mode"; + r#" +import {{ output_text, yield_control }} from "@openai/code_mode"; import {{ exec_command }} from "tools.js"; output_text("phase 1"); +yield_control(); {termination_wait} output_text("phase 2"); "# @@ -1043,22 +1049,24 @@ async fn code_mode_exec_wait_terminate_returns_completed_session_if_it_finished_ let session_a_done_command = format!("printf done > {session_a_done_marker_quoted}"); let session_a_code = format!( - r#"// @exec: {{"yield_time_ms": 10}} -import {{ output_text }} from "@openai/code_mode"; + r#" +import {{ output_text, yield_control }} from "@openai/code_mode"; import {{ exec_command }} from "tools.js"; output_text("session a start"); +yield_control(); {session_a_wait} output_text("session a done"); await exec_command({{ cmd: {session_a_done_command:?} }}); "# ); let session_b_code = format!( - r#"// @exec: {{"yield_time_ms": 10}} -import {{ output_text }} from "@openai/code_mode"; + r#" +import {{ output_text, yield_control }} from "@openai/code_mode"; import {{ exec_command }} from "tools.js"; output_text("session b start"); +yield_control(); {session_b_wait} output_text("session b done"); "# @@ -1332,11 +1340,12 @@ async fn code_mode_exec_wait_uses_its_own_max_tokens_budget() -> Result<()> { let completion_wait = wait_for_file_source(&completion_gate)?; let code = format!( - r#"// @exec: {{"max_output_tokens": 100, "yield_time_ms": 10}} -import {{ output_text }} from "@openai/code_mode"; + r#"// @exec: {{"max_output_tokens": 100}} +import {{ output_text, yield_control }} from "@openai/code_mode"; import {{ exec_command }} from "tools.js"; output_text("phase 1"); +yield_control(); {completion_wait} output_text("token one token two token three token four token five token six token seven"); "# @@ -1869,14 +1878,21 @@ add_content(JSON.stringify(tool)); ); let parsed: Value = serde_json::from_str(&output)?; - assert_eq!( - parsed, - serde_json::json!({ - "module": "tools.js", - "name": "view_image", - "description": "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags).\n\nCode mode declaration:\n```ts\nimport { view_image } from \"tools.js\";\ndeclare function view_image(args: {\n path: string;\n}): Promise;\n```", - }) + assert_eq!(parsed.get("module"), Some(&serde_json::json!("tools.js"))); + assert_eq!(parsed.get("name"), Some(&serde_json::json!("view_image"))); + let description = parsed + .get("description") + .and_then(Value::as_str) + .expect("tool metadata should include a description"); + assert!( + description.starts_with( + "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags)." + ) ); + assert!(description.contains("import { view_image } from \"tools.js\";")); + assert!(description.contains("declare function view_image(args: {")); + assert!(description.contains("path: string;")); + assert!(description.contains("}): Promise;")); Ok(()) } @@ -1908,13 +1924,23 @@ add_content(JSON.stringify(tool)); let parsed: Value = serde_json::from_str(&output)?; assert_eq!( - parsed, - serde_json::json!({ - "module": "tools/mcp/rmcp.js", - "name": "echo", - "description": "Echo back the provided message and include environment data.\n\nCode mode declaration:\n```ts\nimport { echo } from \"tools/mcp/rmcp.js\";\ndeclare function echo(args: {\n env_var?: string;\n message: string;\n}): Promise<{\n _meta?: unknown;\n content: Array;\n isError?: boolean;\n structuredContent?: unknown;\n}>;\n```", - }) + parsed.get("module"), + Some(&serde_json::json!("tools/mcp/rmcp.js")) + ); + assert_eq!(parsed.get("name"), Some(&serde_json::json!("echo"))); + let description = parsed + .get("description") + .and_then(Value::as_str) + .expect("tool metadata should include a description"); + assert!( + description.starts_with("Echo back the provided message and include environment data.") ); + assert!(description.contains("import { echo } from \"tools/mcp/rmcp.js\";")); + assert!(description.contains("declare function echo(args: {")); + assert!(description.contains("env_var?: string;")); + assert!(description.contains("message: string;")); + assert!(description.contains("content: Array;")); + assert!(description.contains("structuredContent?: unknown;")); Ok(()) } From ebd7bbd97af5d2caec8d6e49be23aec745d2cc41 Mon Sep 17 00:00:00 2001 From: Channing Conger Date: Thu, 12 Mar 2026 18:51:57 -0700 Subject: [PATCH 4/6] Fix reconcile --- codex-rs/core/src/tools/code_mode/bridge.js | 72 ++--- .../core/src/tools/code_mode/description.md | 13 +- codex-rs/core/src/tools/code_mode/mod.rs | 2 + codex-rs/core/src/tools/code_mode/protocol.rs | 1 + codex-rs/core/src/tools/code_mode/runner.cjs | 114 ++++++-- .../core/src/tools/code_mode_description.rs | 7 +- .../src/tools/code_mode_description_tests.rs | 6 +- codex-rs/core/src/tools/spec_tests.rs | 4 +- codex-rs/core/tests/suite/code_mode.rs | 269 ++++++------------ 9 files changed, 221 insertions(+), 267 deletions(-) diff --git a/codex-rs/core/src/tools/code_mode/bridge.js b/codex-rs/core/src/tools/code_mode/bridge.js index d7967faabe7..ab353feedeb 100644 --- a/codex-rs/core/src/tools/code_mode/bridge.js +++ b/codex-rs/core/src/tools/code_mode/bridge.js @@ -1,7 +1,9 @@ -const __codexEnabledTools = __CODE_MODE_ENABLED_TOOLS_PLACEHOLDER__; const __codexContentItems = Array.isArray(globalThis.__codexContentItems) ? globalThis.__codexContentItems : []; +const __codexRuntime = globalThis.__codexRuntime; + +delete globalThis.__codexRuntime; Object.defineProperty(globalThis, '__codexContentItems', { value: __codexContentItems, @@ -11,53 +13,37 @@ Object.defineProperty(globalThis, '__codexContentItems', { }); (() => { - function cloneContentItem(item) { - if (!item || typeof item !== 'object') { - throw new TypeError('content item must be an object'); - } - switch (item.type) { - case 'input_text': - if (typeof item.text !== 'string') { - throw new TypeError('content item "input_text" requires a string text field'); - } - return { type: 'input_text', text: item.text }; - case 'input_image': - if (typeof item.image_url !== 'string') { - throw new TypeError('content item "input_image" requires a string image_url field'); - } - return { type: 'input_image', image_url: item.image_url }; - default: - throw new TypeError(`unsupported content item type "${item.type}"`); - } - } - - function normalizeRawContentItems(value) { - if (Array.isArray(value)) { - return value.flatMap((entry) => normalizeRawContentItems(entry)); - } - return [cloneContentItem(value)]; + if (!__codexRuntime || typeof __codexRuntime !== 'object') { + throw new Error('code mode runtime is unavailable'); } - function normalizeContentItems(value) { - if (typeof value === 'string') { - return [{ type: 'input_text', text: value }]; - } - return normalizeRawContentItems(value); + function defineGlobal(name, value) { + Object.defineProperty(globalThis, name, { + value, + configurable: true, + enumerable: true, + writable: false, + }); } - globalThis.add_content = (value) => { - const contentItems = normalizeContentItems(value); - __codexContentItems.push(...contentItems); - return contentItems; - }; + defineGlobal('ALL_TOOLS', __codexRuntime.ALL_TOOLS); + defineGlobal('image', __codexRuntime.image); + defineGlobal('load', __codexRuntime.load); + defineGlobal('store', __codexRuntime.store); + defineGlobal('text', __codexRuntime.text); + defineGlobal('tools', __codexRuntime.tools); + defineGlobal('yield_control', __codexRuntime.yield_control); - globalThis.console = Object.freeze({ - log() {}, - info() {}, - warn() {}, - error() {}, - debug() {}, - }); + defineGlobal( + 'console', + Object.freeze({ + log() {}, + info() {}, + warn() {}, + error() {}, + debug() {}, + }) + ); })(); __CODE_MODE_USER_CODE_PLACEHOLDER__ diff --git a/codex-rs/core/src/tools/code_mode/description.md b/codex-rs/core/src/tools/code_mode/description.md index a611d69e3a0..6aa21c5dc18 100644 --- a/codex-rs/core/src/tools/code_mode/description.md +++ b/codex-rs/core/src/tools/code_mode/description.md @@ -4,15 +4,14 @@ - You may optionally start the tool input with a first-line pragma like `// @exec: {"yield_time_ms": 10000, "max_output_tokens": 1000}`. - `yield_time_ms` asks `exec` to yield early after that many milliseconds if the script is still running. - `max_output_tokens` sets the token budget for direct `exec` results. By default the result is truncated to 10000 tokens. -- You have a set of tools provided to you. They are imported either from `tools.js` or `/mcp/server.js` +- All nested tools are available on the global `tools` object, for example `await tools.exec_command(...)`. Tool names are exposed as normalized JavaScript identifiers, for example `await tools.mcp__ologs__get_profile(...)`. - Tool methods take either string or object as parameter. - They return either a structured value or a string based on the description above. -- Surface text back to the model with `output_text(v: string | number | boolean | undefined | null)`. A string representation of the value is returned to the model. Manually serialize complex values. - -- Methods available in `@openai/code_mode` module: -- `output_text(value: string | number | boolean | undefined | null)`: A string representation of the value is returned to the model. Manually serialize complex values. -- `output_image(imageUrl: string)`: An image is returned to the model. `image_url` can be an HTTPS URL or a base64-encoded `data:` URL. -- `store(key: string, value: any)`: stores a serializeable value under a string key for later `exec` calls in the same session. +- Global helpers: +- `text(value: string | number | boolean | undefined | null)`: Appends a text item and returns it. Non-string values are stringified with `JSON.stringify(...)` when possible. +- `image(imageUrl: string)`: Appends an image item and returns it. `image_url` can be an HTTPS URL or a base64-encoded `data:` URL. +- `store(key: string, value: any)`: stores a serializable value under a string key for later `exec` calls in the same session. - `load(key: string)`: returns the stored value for a string key, or `undefined` if it is missing. +- `ALL_TOOLS`: metadata for the enabled nested tools as `{ name, description }` entries. - `yield_control()`: yields the accumulated output to the model immediately while the script keeps running. diff --git a/codex-rs/core/src/tools/code_mode/mod.rs b/codex-rs/core/src/tools/code_mode/mod.rs index 9c18e992a31..ae362693e07 100644 --- a/codex-rs/core/src/tools/code_mode/mod.rs +++ b/codex-rs/core/src/tools/code_mode/mod.rs @@ -223,6 +223,7 @@ fn enabled_tool_from_spec(spec: ToolSpec) -> Option { } let reference = code_mode_tool_reference(&tool_name); + let global_name = normalize_code_mode_identifier(&tool_name); let (description, kind) = match spec { ToolSpec::Function(tool) => (tool.description, protocol::CodeModeToolKind::Function), ToolSpec::Freeform(tool) => (tool.description, protocol::CodeModeToolKind::Freeform), @@ -236,6 +237,7 @@ fn enabled_tool_from_spec(spec: ToolSpec) -> Option { Some(protocol::EnabledTool { tool_name, + global_name, module_path: reference.module_path, namespace: reference.namespace, name: normalize_code_mode_identifier(&reference.tool_key), diff --git a/codex-rs/core/src/tools/code_mode/protocol.rs b/codex-rs/core/src/tools/code_mode/protocol.rs index 81ff8fada06..fc0a497eacc 100644 --- a/codex-rs/core/src/tools/code_mode/protocol.rs +++ b/codex-rs/core/src/tools/code_mode/protocol.rs @@ -17,6 +17,7 @@ pub(super) enum CodeModeToolKind { #[derive(Clone, Debug, Serialize)] pub(super) struct EnabledTool { pub(super) tool_name: String, + pub(super) global_name: String, #[serde(rename = "module")] pub(super) module_path: String, pub(super) namespace: Vec, diff --git a/codex-rs/core/src/tools/code_mode/runner.cjs b/codex-rs/core/src/tools/code_mode/runner.cjs index 77053c71ee4..186b183d860 100644 --- a/codex-rs/core/src/tools/code_mode/runner.cjs +++ b/codex-rs/core/src/tools/code_mode/runner.cjs @@ -115,17 +115,27 @@ function codeModeWorkerMain() { return contentItems; } - function createToolsNamespace(callTool, enabledTools) { + function createGlobalToolsNamespace(callTool, enabledTools) { const tools = Object.create(null); - for (const tool of enabledTools) { - const toolNamespace = Array.isArray(tool.namespace) ? tool.namespace : []; - if (toolNamespace.length !== 0) { - continue; - } + for (const { tool_name, global_name } of enabledTools) { + Object.defineProperty(tools, global_name, { + value: async (args) => callTool(tool_name, args), + configurable: false, + enumerable: true, + writable: false, + }); + } - Object.defineProperty(tools, tool.name, { - value: async (args) => callTool(tool.tool_name, args), + return Object.freeze(tools); + } + + function createModuleToolsNamespace(callTool, enabledTools) { + const tools = Object.create(null); + + for (const { tool_name, global_name } of enabledTools) { + Object.defineProperty(tools, global_name, { + value: async (args) => callTool(tool_name, args), configurable: false, enumerable: true, writable: false, @@ -137,10 +147,9 @@ function codeModeWorkerMain() { function createAllToolsMetadata(enabledTools) { return Object.freeze( - enabledTools.map(({ module: modulePath, name, description }) => + enabledTools.map(({ global_name, description }) => Object.freeze({ - module: modulePath, - name, + name: global_name, description, }) ) @@ -148,9 +157,16 @@ function codeModeWorkerMain() { } function createToolsModule(context, callTool, enabledTools) { - const tools = createToolsNamespace(callTool, enabledTools); + const tools = createModuleToolsNamespace(callTool, enabledTools); const allTools = createAllToolsMetadata(enabledTools); - const exportNames = ['ALL_TOOLS', ...Object.keys(tools)]; + const exportNames = ['ALL_TOOLS']; + + for (const { global_name } of enabledTools) { + if (global_name !== 'ALL_TOOLS') { + exportNames.push(global_name); + } + } + const uniqueExportNames = [...new Set(exportNames)]; return new SyntheticModule( @@ -198,15 +214,15 @@ function codeModeWorkerMain() { function normalizeOutputImageUrl(value) { if (typeof value !== 'string' || !value) { - throw new TypeError('output_image expects a non-empty image URL string'); + throw new TypeError('image expects a non-empty image URL string'); } if (/^(?:https?:\/\/|data:)/i.test(value)) { return value; } - throw new TypeError('output_image expects an http(s) or data URL'); + throw new TypeError('image expects an http(s) or data URL'); } - function createCodeModeModule(context, state) { + function createCodeModeHelpers(context, state) { const load = (key) => { if (typeof key !== 'string') { throw new TypeError('load key must be a string'); @@ -222,7 +238,7 @@ function codeModeWorkerMain() { } state.storedValues[key] = cloneJsonValue(value); }; - const outputText = (value) => { + const text = (value) => { const item = { type: 'input_text', text: serializeOutputText(value), @@ -230,7 +246,7 @@ function codeModeWorkerMain() { ensureContentItems(context).push(item); return item; }; - const outputImage = (value) => { + const image = (value) => { const item = { type: 'input_image', image_url: normalizeOutputImageUrl(value), @@ -238,21 +254,49 @@ function codeModeWorkerMain() { ensureContentItems(context).push(item); return item; }; + const yieldControl = () => { + parentPort.postMessage({ type: 'yield' }); + }; + + return Object.freeze({ + image, + load, + output_image: image, + output_text: outputText, + store, + text, + yield_control: yieldControl, + }); + } + + function createCodeModeModule(context, helpers) { return new SyntheticModule( - ['load', 'output_text', 'output_image', 'store', 'yield_control'], + ['image', 'load', 'output_text', 'output_image', 'store', 'text', 'yield_control'], function initCodeModeModule() { - this.setExport('load', load); - this.setExport('output_text', outputText); - this.setExport('output_image', outputImage); - this.setExport('store', store); - this.setExport('yield_control', () => { - parentPort.postMessage({ type: 'yield' }); - }); + this.setExport('image', helpers.image); + this.setExport('load', helpers.load); + this.setExport('output_text', helpers.output_text); + this.setExport('output_image', helpers.output_image); + this.setExport('store', helpers.store); + this.setExport('text', helpers.text); + this.setExport('yield_control', helpers.yield_control); }, { context } ); } + function createBridgeRuntime(callTool, enabledTools, helpers) { + return Object.freeze({ + ALL_TOOLS: createAllToolsMetadata(enabledTools), + image: helpers.image, + load: helpers.load, + store: helpers.store, + text: helpers.text, + tools: createGlobalToolsNamespace(callTool, enabledTools), + yield_control: helpers.yield_control, + }); + } + function namespacesMatch(left, right) { if (left.length !== right.length) { return false; @@ -303,7 +347,7 @@ function codeModeWorkerMain() { ); } - function createModuleResolver(context, callTool, enabledTools, state) { + function createModuleResolver(context, callTool, enabledTools, helpers) { let toolsModule; let codeModeModule; const namespacedModules = new Map(); @@ -314,7 +358,7 @@ function codeModeWorkerMain() { return toolsModule; } if (specifier === '@openai/code_mode' || specifier === 'openai/code_mode') { - codeModeModule ??= createCodeModeModule(context, state); + codeModeModule ??= createCodeModeModule(context, helpers); return codeModeModule; } const namespacedMatch = /^tools\/(.+)\.js$/.exec(specifier); @@ -358,12 +402,12 @@ function codeModeWorkerMain() { return module; } - async function runModule(context, start, callTool, state) { + async function runModule(context, start, callTool, helpers) { const resolveModule = createModuleResolver( context, callTool, start.enabled_tools ?? [], - state + helpers ); const mainModule = new SourceTextModule(start.source, { context, @@ -382,13 +426,21 @@ function codeModeWorkerMain() { storedValues: cloneJsonValue(start.stored_values ?? {}), }; const callTool = createToolCaller(); + const enabledTools = start.enabled_tools ?? []; const contentItems = createContentItems(); const context = vm.createContext({ __codexContentItems: contentItems, }); + const helpers = createCodeModeHelpers(context, state); + Object.defineProperty(context, '__codexRuntime', { + value: createBridgeRuntime(callTool, enabledTools, helpers), + configurable: true, + enumerable: false, + writable: false, + }); try { - await runModule(context, start, callTool, state); + await runModule(context, start, callTool, helpers); parentPort.postMessage({ type: 'result', stored_values: state.storedValues, diff --git a/codex-rs/core/src/tools/code_mode_description.rs b/codex-rs/core/src/tools/code_mode_description.rs index aaf1d5c46ca..c5657fcacea 100644 --- a/codex-rs/core/src/tools/code_mode_description.rs +++ b/codex-rs/core/src/tools/code_mode_description.rs @@ -74,12 +74,9 @@ fn append_code_mode_sample( input_type: String, output_type: String, ) -> String { - let reference = code_mode_tool_reference(tool_name); - let import_name = normalize_code_mode_identifier(&reference.tool_key); let declaration = format!( - "import {{ {import_name} }} from \"{}\";\ndeclare function {}", - reference.module_path, - render_code_mode_tool_declaration(&reference.tool_key, input_name, input_type, output_type) + "declare const tools: {{\n {}\n}};", + render_code_mode_tool_declaration(tool_name, input_name, input_type, output_type) ); format!("{description}\n\nCode mode declaration:\n```ts\n{declaration}\n```") } diff --git a/codex-rs/core/src/tools/code_mode_description_tests.rs b/codex-rs/core/src/tools/code_mode_description_tests.rs index 8867e3cc27d..d014fc40984 100644 --- a/codex-rs/core/src/tools/code_mode_description_tests.rs +++ b/codex-rs/core/src/tools/code_mode_description_tests.rs @@ -76,7 +76,7 @@ fn render_json_schema_to_typescript_sorts_object_properties() { } #[test] -fn append_code_mode_sample_uses_module_imports_for_valid_identifiers() { +fn append_code_mode_sample_uses_global_tools_for_valid_identifiers() { assert_eq!( append_code_mode_sample( "desc", @@ -85,7 +85,7 @@ fn append_code_mode_sample_uses_module_imports_for_valid_identifiers() { "{ foo: string }".to_string(), "unknown".to_string(), ), - "desc\n\nCode mode declaration:\n```ts\nimport { get_profile } from \"tools/mcp/ologs.js\";\ndeclare function get_profile(args: { foo: string }): Promise;\n```" + "desc\n\nCode mode declaration:\n```ts\ndeclare const tools: {\n mcp__ologs__get_profile(args: { foo: string }): Promise;\n};\n```" ); } @@ -99,6 +99,6 @@ fn append_code_mode_sample_normalizes_invalid_identifiers() { "{ foo: string }".to_string(), "unknown".to_string(), ), - "desc\n\nCode mode declaration:\n```ts\nimport { echo_tool } from \"tools/mcp/rmcp.js\";\ndeclare function echo_tool(args: { foo: string }): Promise;\n```" + "desc\n\nCode mode declaration:\n```ts\ndeclare const tools: {\n mcp__rmcp__echo_tool(args: { foo: string }): Promise;\n};\n```" ); } diff --git a/codex-rs/core/src/tools/spec_tests.rs b/codex-rs/core/src/tools/spec_tests.rs index 07352aafa00..30147ffc90d 100644 --- a/codex-rs/core/src/tools/spec_tests.rs +++ b/codex-rs/core/src/tools/spec_tests.rs @@ -2446,7 +2446,7 @@ fn code_mode_augments_builtin_tool_descriptions_with_typed_sample() { assert_eq!( description, - "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags).\n\nCode mode declaration:\n```ts\nimport { view_image } from \"tools.js\";\ndeclare function view_image(args: {\n path: string;\n }): Promise;\n```" + "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags).\n\nCode mode declaration:\n```ts\ndeclare const tools: {\n view_image(args: {\n path: string;\n }): Promise;\n};\n```" ); } @@ -2498,7 +2498,7 @@ fn code_mode_augments_mcp_tool_descriptions_with_namespaced_sample() { assert_eq!( description, - "Echo text\n\nCode mode declaration:\n```ts\nimport { echo } from \"tools/mcp/sample.js\";\ndeclare function echo(args: {\n message: string;\n }): Promise<{\n _meta?: unknown;\n content: Array;\n isError?: boolean;\n structuredContent?: unknown;\n }>;\n```" + "Echo text\n\nCode mode declaration:\n```ts\ndeclare const tools: {\n mcp__sample__echo(args: {\n message: string;\n }): Promise<{\n _meta?: unknown;\n content: Array;\n isError?: boolean;\n structuredContent?: unknown;\n }>;\n};\n```" ); } diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 8f473a93e9b..8c340f83a4b 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -65,7 +65,7 @@ fn wait_for_file_source(path: &Path) -> Result { let quoted_path = shlex::try_join([path.to_string_lossy().as_ref()])?; let command = format!("if [ -f {quoted_path} ]; then printf ready; fi"); Ok(format!( - r#"while ((await exec_command({{ cmd: {command:?} }})).output !== "ready") {{ + r#"while ((await tools.exec_command({{ cmd: {command:?} }})).output !== "ready") {{ }}"# )) } @@ -199,9 +199,7 @@ async fn code_mode_can_return_exec_command_output() -> Result<()> { &server, "use exec to run exec_command", r#" -import { exec_command } from "tools.js"; - -add_content(JSON.stringify(await exec_command({ cmd: "printf code_mode_exec_marker" }))); +text(JSON.stringify(await tools.exec_command({ cmd: "printf code_mode_exec_marker" }))); "#, false, ) @@ -249,8 +247,6 @@ async fn code_mode_nested_tool_calls_can_run_in_parallel() -> Result<()> { let test = builder.build(&server).await?; let warmup_code = r#" -import { test_sync_tool } from "tools.js"; - const args = { sleep_after_ms: 10, barrier: { @@ -261,13 +257,11 @@ const args = { }; await Promise.all([ - test_sync_tool(args), - test_sync_tool(args), + tools.test_sync_tool(args), + tools.test_sync_tool(args), ]); "#; let code = r#" -import { test_sync_tool } from "tools.js"; - const args = { sleep_after_ms: 300, barrier: { @@ -278,11 +272,11 @@ const args = { }; const results = await Promise.all([ - test_sync_tool(args), - test_sync_tool(args), + tools.test_sync_tool(args), + tools.test_sync_tool(args), ]); -add_content(JSON.stringify(results)); +text(JSON.stringify(results)); "#; let response_mock = responses::mount_sse_sequence( @@ -341,9 +335,7 @@ async fn code_mode_can_truncate_final_result_with_configured_budget() -> Result< &server, "use exec to truncate the final result", r#"// @exec: {"max_output_tokens": 6} -import { exec_command } from "tools.js"; - -add_content(JSON.stringify(await exec_command({ +text(JSON.stringify(await tools.exec_command({ cmd: "printf 'token one token two token three token four token five token six token seven'", max_output_tokens: 100 }))); @@ -383,8 +375,8 @@ async fn code_mode_returns_accumulated_output_when_script_fails() -> Result<()> &server, "use code_mode to surface script failures", r#" -add_content("before crash"); -add_content("still before crash"); +text("before crash"); +text("still before crash"); throw new Error("boom"); "#, false, @@ -434,15 +426,12 @@ async fn code_mode_can_yield_and_resume_with_exec_wait() -> Result<()> { let code = format!( r#" -import {{ output_text, yield_control }} from "@openai/code_mode"; -import {{ exec_command }} from "tools.js"; - -output_text("phase 1"); +text("phase 1"); yield_control(); {phase_2_wait} -output_text("phase 2"); +text("phase 2"); {phase_3_wait} -output_text("phase 3"); +text("phase 3"); "# ); @@ -578,9 +567,7 @@ async fn code_mode_yield_timeout_works_for_busy_loop() -> Result<()> { let test = builder.build(&server).await?; let code = r#"// @exec: {"yield_time_ms": 100} -import { output_text } from "@openai/code_mode"; - -output_text("phase 1"); +text("phase 1"); while (true) {} "#; @@ -679,24 +666,18 @@ async fn code_mode_can_run_multiple_yielded_sessions() -> Result<()> { let session_a_code = format!( r#" -import {{ output_text, yield_control }} from "@openai/code_mode"; -import {{ exec_command }} from "tools.js"; - -output_text("session a start"); +text("session a start"); yield_control(); {session_a_wait} -output_text("session a done"); +text("session a done"); "# ); let session_b_code = format!( r#" -import {{ output_text, yield_control }} from "@openai/code_mode"; -import {{ exec_command }} from "tools.js"; - -output_text("session b start"); +text("session b start"); yield_control(); {session_b_wait} -output_text("session b done"); +text("session b done"); "# ); @@ -851,13 +832,10 @@ async fn code_mode_exec_wait_can_terminate_and_continue() -> Result<()> { let code = format!( r#" -import {{ output_text, yield_control }} from "@openai/code_mode"; -import {{ exec_command }} from "tools.js"; - -output_text("phase 1"); +text("phase 1"); yield_control(); {termination_wait} -output_text("phase 2"); +text("phase 2"); "# ); @@ -933,9 +911,7 @@ output_text("phase 2"); "call-3", "exec", r#" -import { output_text } from "@openai/code_mode"; - -output_text("after terminate"); +text("after terminate"); "#, ), ev_completed("resp-5"), @@ -1050,25 +1026,19 @@ async fn code_mode_exec_wait_terminate_returns_completed_session_if_it_finished_ let session_a_code = format!( r#" -import {{ output_text, yield_control }} from "@openai/code_mode"; -import {{ exec_command }} from "tools.js"; - -output_text("session a start"); +text("session a start"); yield_control(); {session_a_wait} -output_text("session a done"); -await exec_command({{ cmd: {session_a_done_command:?} }}); +text("session a done"); +await tools.exec_command({{ cmd: {session_a_done_command:?} }}); "# ); let session_b_code = format!( r#" -import {{ output_text, yield_control }} from "@openai/code_mode"; -import {{ exec_command }} from "tools.js"; - -output_text("session b start"); +text("session b start"); yield_control(); {session_b_wait} -output_text("session b done"); +text("session b done"); "# ); @@ -1247,13 +1217,10 @@ async fn code_mode_background_keeps_running_on_later_turn_without_exec_wait() -> format!("while [ ! -f {resumed_file_quoted} ]; do sleep 0.01; done; printf ready"); let code = format!( r#" -import {{ yield_control, output_text }} from "@openai/code_mode"; -import {{ exec_command }} from "tools.js"; - -output_text("before yield"); +text("before yield"); yield_control(); -await exec_command({{ cmd: {write_file_command:?} }}); -output_text("after yield"); +await tools.exec_command({{ cmd: {write_file_command:?} }}); +text("after yield"); "# ); @@ -1341,13 +1308,10 @@ async fn code_mode_exec_wait_uses_its_own_max_tokens_budget() -> Result<()> { let code = format!( r#"// @exec: {{"max_output_tokens": 100}} -import {{ output_text, yield_control }} from "@openai/code_mode"; -import {{ exec_command }} from "tools.js"; - -output_text("phase 1"); +text("phase 1"); yield_control(); {completion_wait} -output_text("token one token two token three token four token five token six token seven"); +text("token one token two token three token four token five token six token seven"); "# ); @@ -1429,7 +1393,7 @@ Total\ output\ lines:\ 1\n } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn code_mode_can_output_serialized_text_via_openai_code_mode_module() -> Result<()> { +async fn code_mode_can_output_serialized_text_via_global_helper() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1437,9 +1401,7 @@ async fn code_mode_can_output_serialized_text_via_openai_code_mode_module() -> R &server, "use exec to return structured text", r#" -import { output_text } from "@openai/code_mode"; - -output_text({ json: true }); +text({ json: true }); "#, false, ) @@ -1458,7 +1420,7 @@ output_text({ json: true }); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn code_mode_surfaces_output_text_stringify_errors() -> Result<()> { +async fn code_mode_surfaces_text_stringify_errors() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1466,11 +1428,9 @@ async fn code_mode_surfaces_output_text_stringify_errors() -> Result<()> { &server, "use exec to return circular text", r#" -import { output_text } from "@openai/code_mode"; - const circular = {}; circular.self = circular; -output_text(circular); +text(circular); "#, false, ) @@ -1501,7 +1461,7 @@ output_text(circular); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn code_mode_can_output_images_via_openai_code_mode_module() -> Result<()> { +async fn code_mode_can_output_images_via_global_helper() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; @@ -1509,10 +1469,8 @@ async fn code_mode_can_output_images_via_openai_code_mode_module() -> Result<()> &server, "use exec to return images", r#" -import { output_image } from "@openai/code_mode"; - -output_image("https://example.com/image.jpg"); -output_image("data:image/png;base64,AAA"); +image("https://example.com/image.jpg"); +image("data:image/png;base64,AAA"); "#, false, ) @@ -1561,9 +1519,7 @@ async fn code_mode_can_apply_patch_via_nested_tool() -> Result<()> { let patch = format!( "*** Begin Patch\n*** Add File: {file_name}\n+hello from code_mode\n*** End Patch\n" ); - let code = format!( - "import {{ apply_patch }} from \"tools.js\";\nconst items = await apply_patch({patch:?});\nadd_content(items);\n" - ); + let code = format!("text(await tools.apply_patch({patch:?}));\n"); let (test, second_mock) = run_code_mode_turn(&server, "use exec to run apply_patch", &code, true).await?; @@ -1599,12 +1555,10 @@ async fn code_mode_can_print_structured_mcp_tool_result_fields() -> Result<()> { let server = responses::start_mock_server().await; let code = r#" -import { echo } from "tools/mcp/rmcp.js"; - -const { content, structuredContent, isError } = await echo({ +const { content, structuredContent, isError } = await tools.mcp__rmcp__echo({ message: "ping", }); -add_content( +text( `echo=${structuredContent?.echo ?? "missing"}\n` + `env=${structuredContent?.env ?? "missing"}\n` + `isError=${String(isError)}\n` + @@ -1634,37 +1588,33 @@ contentLength=0" } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn code_mode_can_dynamically_import_namespaced_mcp_tools() -> Result<()> { +async fn code_mode_exposes_mcp_tools_on_global_tools_object() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; let code = r#" -const rmcp = await import("tools/mcp/rmcp.js"); -const { content, structuredContent, isError } = await rmcp.echo({ +const { content, structuredContent, isError } = await tools.mcp__rmcp__echo({ message: "ping", }); -add_content( - `hasEcho=${String(Object.keys(rmcp).includes("echo"))}\n` + - `echoType=${typeof rmcp.echo}\n` + +text( + `hasEcho=${String(Object.keys(tools).includes("mcp__rmcp__echo"))}\n` + + `echoType=${typeof tools.mcp__rmcp__echo}\n` + `echo=${structuredContent?.echo ?? "missing"}\n` + `isError=${String(isError)}\n` + `contentLength=${content.length}` ); "#; - let (_test, second_mock) = run_code_mode_turn_with_rmcp( - &server, - "use exec to dynamically import the rmcp module", - code, - ) - .await?; + let (_test, second_mock) = + run_code_mode_turn_with_rmcp(&server, "use exec to inspect the global tools object", code) + .await?; let req = second_mock.single_request(); let (output, success) = custom_tool_output_body_and_success(&req, "call-1"); assert_ne!( success, Some(false), - "exec dynamic rmcp import failed unexpectedly: {output}" + "exec global rmcp access failed unexpectedly: {output}" ); assert_eq!( output, @@ -1679,42 +1629,35 @@ contentLength=0" } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn code_mode_does_not_export_namespaced_mcp_tools_from_tools_js() -> Result<()> { +async fn code_mode_exposes_namespaced_mcp_tools_on_global_tools_object() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; let code = r#" -const tools = await import("tools.js"); -const exports = Object.keys(tools).sort(); -add_content(JSON.stringify({ - hasAllTools: exports.includes("ALL_TOOLS"), - hasExecCommand: exports.includes("exec_command"), - namespacedExports: exports.filter((name) => name.startsWith("mcp__")), +text(JSON.stringify({ + hasExecCommand: typeof tools.exec_command === "function", + hasNamespacedEcho: typeof tools.mcp__rmcp__echo === "function", })); "#; - let (_test, second_mock) = run_code_mode_turn_with_rmcp( - &server, - "use exec to inspect the top-level tools module", - code, - ) - .await?; + let (_test, second_mock) = + run_code_mode_turn_with_rmcp(&server, "use exec to inspect the global tools object", code) + .await?; let req = second_mock.single_request(); let (output, success) = custom_tool_output_body_and_success(&req, "call-1"); assert_ne!( success, Some(false), - "exec tools.js inspection failed unexpectedly: {output}" + "exec global tools inspection failed unexpectedly: {output}" ); let parsed: Value = serde_json::from_str(&output)?; assert_eq!( parsed, serde_json::json!({ - "hasAllTools": true, "hasExecCommand": true, - "namespacedExports": [], + "hasNamespacedEcho": true, }) ); @@ -1722,20 +1665,18 @@ add_content(JSON.stringify({ } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn code_mode_normalizes_illegal_namespaced_mcp_tool_identifiers() -> Result<()> { +async fn code_mode_exposes_normalized_illegal_mcp_tool_names() -> Result<()> { skip_if_no_network!(Ok(())); let server = responses::start_mock_server().await; let code = r#" -import { echo_tool } from "tools/mcp/rmcp.js"; - -const result = await echo_tool({ message: "ping" }); -add_content(`echo=${result.structuredContent.echo}`); +const result = await tools.mcp__rmcp__echo_tool({ message: "ping" }); +text(`echo=${result.structuredContent.echo}`); "#; let (_test, second_mock) = run_code_mode_turn_with_rmcp( &server, - "use exec to import a normalized rmcp tool name", + "use exec to call a normalized rmcp tool name", code, ) .await?; @@ -1745,7 +1686,7 @@ add_content(`echo=${result.structuredContent.echo}`); assert_ne!( success, Some(false), - "exec normalized rmcp import failed unexpectedly: {output}" + "exec normalized rmcp tool call failed unexpectedly: {output}" ); assert_eq!(output, "echo=ECHOING: ping"); @@ -1758,7 +1699,7 @@ async fn code_mode_lists_global_scope_items() -> Result<()> { let server = responses::start_mock_server().await; let code = r#" -add_content(JSON.stringify(Object.getOwnPropertyNames(globalThis).sort())); +text(JSON.stringify(Object.getOwnPropertyNames(globalThis).sort())); "#; let (_test, second_mock) = @@ -1775,6 +1716,7 @@ add_content(JSON.stringify(Object.getOwnPropertyNames(globalThis).sort())); let globals = globals.into_iter().collect::>(); let expected = [ "AggregateError", + "ALL_TOOLS", "Array", "ArrayBuffer", "AsyncDisposableStack", @@ -1837,12 +1779,18 @@ add_content(JSON.stringify(Object.getOwnPropertyNames(globalThis).sort())); "escape", "eval", "globalThis", + "image", "isFinite", "isNaN", + "load", "parseFloat", "parseInt", + "store", + "text", + "tools", "undefined", "unescape", + "yield_control", ]; for g in &globals { assert!( @@ -1860,10 +1808,8 @@ async fn code_mode_exports_all_tools_metadata_for_builtin_tools() -> Result<()> let server = responses::start_mock_server().await; let code = r#" -import { ALL_TOOLS } from "tools.js"; - -const tool = ALL_TOOLS.find(({ module, name }) => module === "tools.js" && name === "view_image"); -add_content(JSON.stringify(tool)); +const tool = ALL_TOOLS.find(({ name }) => name === "view_image"); +text(JSON.stringify(tool)); "#; let (_test, second_mock) = @@ -1878,21 +1824,13 @@ add_content(JSON.stringify(tool)); ); let parsed: Value = serde_json::from_str(&output)?; - assert_eq!(parsed.get("module"), Some(&serde_json::json!("tools.js"))); - assert_eq!(parsed.get("name"), Some(&serde_json::json!("view_image"))); - let description = parsed - .get("description") - .and_then(Value::as_str) - .expect("tool metadata should include a description"); - assert!( - description.starts_with( - "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags)." - ) + assert_eq!( + parsed, + serde_json::json!({ + "name": "view_image", + "description": "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within tags).\n\nCode mode declaration:\n```ts\ndeclare const tools: {\n view_image(args: {\n path: string;\n }): Promise;\n};\n```", + }) ); - assert!(description.contains("import { view_image } from \"tools.js\";")); - assert!(description.contains("declare function view_image(args: {")); - assert!(description.contains("path: string;")); - assert!(description.contains("}): Promise;")); Ok(()) } @@ -1903,12 +1841,10 @@ async fn code_mode_exports_all_tools_metadata_for_namespaced_mcp_tools() -> Resu let server = responses::start_mock_server().await; let code = r#" -import { ALL_TOOLS } from "tools.js"; - const tool = ALL_TOOLS.find( - ({ module, name }) => module === "tools/mcp/rmcp.js" && name === "echo" + ({ name }) => name === "mcp__rmcp__echo" ); -add_content(JSON.stringify(tool)); +text(JSON.stringify(tool)); "#; let (_test, second_mock) = @@ -1924,23 +1860,12 @@ add_content(JSON.stringify(tool)); let parsed: Value = serde_json::from_str(&output)?; assert_eq!( - parsed.get("module"), - Some(&serde_json::json!("tools/mcp/rmcp.js")) - ); - assert_eq!(parsed.get("name"), Some(&serde_json::json!("echo"))); - let description = parsed - .get("description") - .and_then(Value::as_str) - .expect("tool metadata should include a description"); - assert!( - description.starts_with("Echo back the provided message and include environment data.") + parsed, + serde_json::json!({ + "name": "mcp__rmcp__echo", + "description": "Echo back the provided message and include environment data.\n\nCode mode declaration:\n```ts\ndeclare const tools: {\n mcp__rmcp__echo(args: {\n env_var?: string;\n message: string;\n }): Promise<{\n _meta?: unknown;\n content: Array;\n isError?: boolean;\n structuredContent?: unknown;\n }>;\n};\n```", + }) ); - assert!(description.contains("import { echo } from \"tools/mcp/rmcp.js\";")); - assert!(description.contains("declare function echo(args: {")); - assert!(description.contains("env_var?: string;")); - assert!(description.contains("message: string;")); - assert!(description.contains("content: Array;")); - assert!(description.contains("structuredContent?: unknown;")); Ok(()) } @@ -1951,13 +1876,11 @@ async fn code_mode_can_print_content_only_mcp_tool_result_fields() -> Result<()> let server = responses::start_mock_server().await; let code = r#" -import { image_scenario } from "tools/mcp/rmcp.js"; - -const { content, structuredContent, isError } = await image_scenario({ +const { content, structuredContent, isError } = await tools.mcp__rmcp__image_scenario({ scenario: "text_only", caption: "caption from mcp", }); -add_content( +text( `firstType=${content[0]?.type ?? "missing"}\n` + `firstText=${content[0]?.text ?? "missing"}\n` + `structuredContent=${String(structuredContent ?? null)}\n` + @@ -1996,13 +1919,11 @@ async fn code_mode_can_print_error_mcp_tool_result_fields() -> Result<()> { let server = responses::start_mock_server().await; let code = r#" -import { echo } from "tools/mcp/rmcp.js"; - -const { content, structuredContent, isError } = await echo({}); +const { content, structuredContent, isError } = await tools.mcp__rmcp__echo({}); const firstText = content[0]?.text ?? ""; const mentionsMissingMessage = firstText.includes("missing field") && firstText.includes("message"); -add_content( +text( `isError=${String(isError)}\n` + `contentLength=${content.length}\n` + `mentionsMissingMessage=${String(mentionsMissingMessage)}\n` + @@ -2049,10 +1970,8 @@ async fn code_mode_can_store_and_load_values_across_turns() -> Result<()> { "call-1", "exec", r#" -import { store } from "@openai/code_mode"; - store("nb", { title: "Notebook", items: [1, true, null] }); -add_content("stored"); +text("stored"); "#, ), ev_completed("resp-1"), @@ -2088,9 +2007,7 @@ add_content("stored"); "call-2", "exec", r#" -import { load } from "openai/code_mode"; - -add_content(JSON.stringify(load("nb"))); +text(JSON.stringify(load("nb"))); "#, ), ev_completed("resp-3"), From 9e32f3cfc6e8fbfe3798dfce78f8a3f0436a8ac8 Mon Sep 17 00:00:00 2001 From: Channing Conger Date: Thu, 12 Mar 2026 19:30:01 -0700 Subject: [PATCH 5/6] name commit --- codex-rs/core/src/tools/code_mode/runner.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/core/src/tools/code_mode/runner.cjs b/codex-rs/core/src/tools/code_mode/runner.cjs index 186b183d860..a5d457e5bc4 100644 --- a/codex-rs/core/src/tools/code_mode/runner.cjs +++ b/codex-rs/core/src/tools/code_mode/runner.cjs @@ -262,7 +262,7 @@ function codeModeWorkerMain() { image, load, output_image: image, - output_text: outputText, + output_text: text, store, text, yield_control: yieldControl, From 90a8162b3c4bba6b326c793e525d05996712684b Mon Sep 17 00:00:00 2001 From: Channing Conger Date: Thu, 12 Mar 2026 20:02:36 -0700 Subject: [PATCH 6/6] Fix windows exec --- codex-rs/core/tests/suite/code_mode.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 8c340f83a4b..5b2fe6376b7 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -1656,7 +1656,7 @@ text(JSON.stringify({ assert_eq!( parsed, serde_json::json!({ - "hasExecCommand": true, + "hasExecCommand": !cfg!(windows), "hasNamespacedEcho": true, }) );