mirror of
https://github.com/openai/codex.git
synced 2026-05-25 05:24:37 +00:00
Expose code-mode tools through globals (#14517)
Summary - make all code-mode tools accessible as globals so callers only need `tools.<name>` - rename text/image helpers and key globals (store, load, ALL_TOOLS, etc.) to reflect the new shared namespace - update the JS bridge, runners, descriptions, router, and tests to follow the new API Testing - Not run (not requested)
This commit is contained in:
@@ -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,42 @@ 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}"`);
|
||||
}
|
||||
if (!__codexRuntime || typeof __codexRuntime !== 'object') {
|
||||
throw new Error('code mode runtime is unavailable');
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
globalThis.console = Object.freeze({
|
||||
log() {},
|
||||
info() {},
|
||||
warn() {},
|
||||
error() {},
|
||||
debug() {},
|
||||
});
|
||||
defineGlobal(
|
||||
'console',
|
||||
Object.freeze({
|
||||
log() {},
|
||||
info() {},
|
||||
warn() {},
|
||||
error() {},
|
||||
debug() {},
|
||||
})
|
||||
);
|
||||
})();
|
||||
|
||||
__CODE_MODE_USER_CODE_PLACEHOLDER__
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
## 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.
|
||||
- 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.
|
||||
- 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 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.
|
||||
|
||||
@@ -280,8 +280,6 @@ async fn call_nested_tool(
|
||||
return JsonValue::String(format!("{PUBLIC_TOOL_NAME} cannot invoke itself"));
|
||||
}
|
||||
|
||||
let router = build_nested_router(&exec).await;
|
||||
let specs = router.specs();
|
||||
let payload =
|
||||
if let Some((server, tool)) = exec.session.parse_mcp_tool_name(&tool_name, &None).await {
|
||||
match serialize_function_tool_arguments(&tool_name, input) {
|
||||
@@ -293,7 +291,7 @@ async fn call_nested_tool(
|
||||
Err(error) => return JsonValue::String(error),
|
||||
}
|
||||
} else {
|
||||
match build_nested_tool_payload(&specs, &tool_name, input) {
|
||||
match build_nested_tool_payload(tool_runtime.find_spec(&tool_name), &tool_name, input) {
|
||||
Ok(payload) => payload,
|
||||
Err(error) => return JsonValue::String(error),
|
||||
}
|
||||
@@ -324,22 +322,20 @@ fn tool_kind_for_spec(spec: &ToolSpec) -> protocol::CodeModeToolKind {
|
||||
}
|
||||
|
||||
fn tool_kind_for_name(
|
||||
specs: &[ToolSpec],
|
||||
spec: Option<ToolSpec>,
|
||||
tool_name: &str,
|
||||
) -> Result<protocol::CodeModeToolKind, String> {
|
||||
specs
|
||||
.iter()
|
||||
.find(|spec| spec.name() == tool_name)
|
||||
spec.as_ref()
|
||||
.map(tool_kind_for_spec)
|
||||
.ok_or_else(|| format!("tool `{tool_name}` is not enabled in {PUBLIC_TOOL_NAME}"))
|
||||
}
|
||||
|
||||
fn build_nested_tool_payload(
|
||||
specs: &[ToolSpec],
|
||||
spec: Option<ToolSpec>,
|
||||
tool_name: &str,
|
||||
input: Option<JsonValue>,
|
||||
) -> Result<ToolPayload, String> {
|
||||
let actual_kind = tool_kind_for_name(specs, tool_name)?;
|
||||
let actual_kind = tool_kind_for_name(spec, tool_name)?;
|
||||
match actual_kind {
|
||||
protocol::CodeModeToolKind::Function => build_function_tool_payload(tool_name, input),
|
||||
protocol::CodeModeToolKind::Freeform => build_freeform_tool_payload(tool_name, input),
|
||||
|
||||
@@ -131,7 +131,22 @@ function codeModeWorkerMain() {
|
||||
return contentItems;
|
||||
}
|
||||
|
||||
function createToolsNamespace(callTool, enabledTools) {
|
||||
function createGlobalToolsNamespace(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_name, global_name } of enabledTools) {
|
||||
@@ -148,10 +163,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,
|
||||
})
|
||||
)
|
||||
@@ -159,7 +173,7 @@ 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'];
|
||||
|
||||
@@ -216,15 +230,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');
|
||||
@@ -240,7 +254,7 @@ function codeModeWorkerMain() {
|
||||
}
|
||||
state.storedValues[key] = cloneJsonValue(value);
|
||||
};
|
||||
const outputText = (value) => {
|
||||
const text = (value) => {
|
||||
const item = {
|
||||
type: 'input_text',
|
||||
text: serializeOutputText(value),
|
||||
@@ -248,7 +262,7 @@ function codeModeWorkerMain() {
|
||||
ensureContentItems(context).push(item);
|
||||
return item;
|
||||
};
|
||||
const outputImage = (value) => {
|
||||
const image = (value) => {
|
||||
const item = {
|
||||
type: 'input_image',
|
||||
image_url: normalizeOutputImageUrl(value),
|
||||
@@ -256,47 +270,85 @@ 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',
|
||||
],
|
||||
function initCodeModeModule() {
|
||||
this.setExport('load', load);
|
||||
this.setExport('output_text', outputText);
|
||||
this.setExport('output_image', outputImage);
|
||||
this.setExport('set_max_output_tokens_per_exec_call', (value) => {
|
||||
const normalized = normalizeMaxOutputTokensPerExecCall(value);
|
||||
state.maxOutputTokensPerExecCall = normalized;
|
||||
parentPort.postMessage({
|
||||
type: 'set_max_output_tokens_per_exec_call',
|
||||
value: normalized,
|
||||
});
|
||||
return normalized;
|
||||
});
|
||||
this.setExport('set_yield_time', (value) => {
|
||||
const normalized = normalizeYieldTime(value);
|
||||
parentPort.postMessage({
|
||||
type: 'set_yield_time',
|
||||
value: normalized,
|
||||
});
|
||||
return normalized;
|
||||
});
|
||||
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(
|
||||
'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);
|
||||
},
|
||||
{ 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;
|
||||
@@ -347,16 +399,18 @@ function codeModeWorkerMain() {
|
||||
);
|
||||
}
|
||||
|
||||
function createModuleResolver(context, callTool, enabledTools, state) {
|
||||
const toolsModule = createToolsModule(context, callTool, enabledTools);
|
||||
const codeModeModule = createCodeModeModule(context, state);
|
||||
function createModuleResolver(context, callTool, enabledTools, helpers) {
|
||||
let toolsModule;
|
||||
let codeModeModule;
|
||||
const namespacedModules = new Map();
|
||||
|
||||
return function resolveModule(specifier) {
|
||||
if (specifier === 'tools.js') {
|
||||
toolsModule ??= createToolsModule(context, callTool, enabledTools);
|
||||
return toolsModule;
|
||||
}
|
||||
if (specifier === '@openai/code_mode' || specifier === 'openai/code_mode') {
|
||||
codeModeModule ??= createCodeModeModule(context, helpers);
|
||||
return codeModeModule;
|
||||
}
|
||||
const namespacedMatch = /^tools\/(.+)\.js$/.exec(specifier);
|
||||
@@ -400,12 +454,12 @@ function codeModeWorkerMain() {
|
||||
return module;
|
||||
}
|
||||
|
||||
async function runModule(context, start, state, callTool) {
|
||||
async function runModule(context, start, callTool, helpers) {
|
||||
const resolveModule = createModuleResolver(
|
||||
context,
|
||||
callTool,
|
||||
start.enabled_tools ?? [],
|
||||
state
|
||||
helpers
|
||||
);
|
||||
const mainModule = new SourceTextModule(start.source, {
|
||||
context,
|
||||
@@ -425,12 +479,21 @@ function codeModeWorkerMain() {
|
||||
storedValues: cloneJsonValue(start.stored_values ?? {}),
|
||||
};
|
||||
const callTool = createToolCaller();
|
||||
const enabledTools = start.enabled_tools ?? [];
|
||||
const contentItems = createContentItems();
|
||||
const context = vm.createContext({
|
||||
__codexContentItems: createContentItems(),
|
||||
__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, state, callTool);
|
||||
await runModule(context, start, callTool, helpers);
|
||||
parentPort.postMessage({
|
||||
type: 'result',
|
||||
stored_values: state.storedValues,
|
||||
|
||||
Reference in New Issue
Block a user