mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add exit helper to code mode scripts (#14851)
- **Summary** - expose `exit` through the code mode bridge and module so scripts can stop mid-flight - surface the helper in the description documentation - add a regression test ensuring `exit()` terminates execution cleanly - **Testing** - Not run (not requested)
This commit is contained in:
committed by
GitHub
Unverified
parent
d0a693e541
commit
a3ba10b44b
@@ -27,6 +27,7 @@ Object.defineProperty(globalThis, '__codexContentItems', {
|
||||
}
|
||||
|
||||
defineGlobal('ALL_TOOLS', __codexRuntime.ALL_TOOLS);
|
||||
defineGlobal('exit', __codexRuntime.exit);
|
||||
defineGlobal('image', __codexRuntime.image);
|
||||
defineGlobal('load', __codexRuntime.load);
|
||||
defineGlobal('store', __codexRuntime.store);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
- They return either a structured value or a string based on the description above.
|
||||
|
||||
- Global helpers:
|
||||
- `exit()`: Immediately ends the current script successfully (like an early return from the top level).
|
||||
- `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.
|
||||
|
||||
@@ -55,6 +55,17 @@ function codeModeWorkerMain() {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
class CodeModeExitSignal extends Error {
|
||||
constructor() {
|
||||
super('code mode exit');
|
||||
this.name = 'CodeModeExitSignal';
|
||||
}
|
||||
}
|
||||
|
||||
function isCodeModeExitSignal(error) {
|
||||
return error instanceof CodeModeExitSignal;
|
||||
}
|
||||
|
||||
function createToolCaller() {
|
||||
let nextId = 0;
|
||||
const pending = new Map();
|
||||
@@ -257,8 +268,12 @@ function codeModeWorkerMain() {
|
||||
const yieldControl = () => {
|
||||
parentPort.postMessage({ type: 'yield' });
|
||||
};
|
||||
const exit = () => {
|
||||
throw new CodeModeExitSignal();
|
||||
};
|
||||
|
||||
return Object.freeze({
|
||||
exit,
|
||||
image,
|
||||
load,
|
||||
output_image: image,
|
||||
@@ -271,8 +286,18 @@ function codeModeWorkerMain() {
|
||||
|
||||
function createCodeModeModule(context, helpers) {
|
||||
return new SyntheticModule(
|
||||
['image', 'load', 'output_text', 'output_image', 'store', 'text', 'yield_control'],
|
||||
[
|
||||
'exit',
|
||||
'image',
|
||||
'load',
|
||||
'output_text',
|
||||
'output_image',
|
||||
'store',
|
||||
'text',
|
||||
'yield_control',
|
||||
],
|
||||
function initCodeModeModule() {
|
||||
this.setExport('exit', helpers.exit);
|
||||
this.setExport('image', helpers.image);
|
||||
this.setExport('load', helpers.load);
|
||||
this.setExport('output_text', helpers.output_text);
|
||||
@@ -288,6 +313,7 @@ function codeModeWorkerMain() {
|
||||
function createBridgeRuntime(callTool, enabledTools, helpers) {
|
||||
return Object.freeze({
|
||||
ALL_TOOLS: createAllToolsMetadata(enabledTools),
|
||||
exit: helpers.exit,
|
||||
image: helpers.image,
|
||||
load: helpers.load,
|
||||
store: helpers.store,
|
||||
@@ -446,6 +472,13 @@ function codeModeWorkerMain() {
|
||||
stored_values: state.storedValues,
|
||||
});
|
||||
} catch (error) {
|
||||
if (isCodeModeExitSignal(error)) {
|
||||
parentPort.postMessage({
|
||||
type: 'result',
|
||||
stored_values: state.storedValues,
|
||||
});
|
||||
return;
|
||||
}
|
||||
parentPort.postMessage({
|
||||
type: 'result',
|
||||
stored_values: state.storedValues,
|
||||
|
||||
@@ -1551,6 +1551,47 @@ text({ json: true });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn code_mode_exit_stops_script_immediately() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = responses::start_mock_server().await;
|
||||
let (_test, second_mock) = run_code_mode_turn(
|
||||
&server,
|
||||
"use exec to stop script early with exit helper",
|
||||
r#"
|
||||
import { exit, text } from "@openai/code_mode";
|
||||
|
||||
text("before");
|
||||
exit();
|
||||
text("after");
|
||||
"#,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let req = second_mock.single_request();
|
||||
let items = custom_tool_output_items(&req, "call-1");
|
||||
let (output, success) = custom_tool_output_body_and_success(&req, "call-1");
|
||||
assert_ne!(
|
||||
success,
|
||||
Some(false),
|
||||
"exec exit helper call failed unexpectedly: {output}"
|
||||
);
|
||||
assert_eq!(items.len(), 2);
|
||||
assert_regex_match(
|
||||
concat!(
|
||||
r"(?s)\A",
|
||||
r"Script completed\nWall time \d+\.\d seconds\nOutput:\n\z"
|
||||
),
|
||||
text_item(&items, 0),
|
||||
);
|
||||
assert_eq!(text_item(&items, 1), "before");
|
||||
assert_eq!(output, "before");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn code_mode_surfaces_text_stringify_errors() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
@@ -1909,6 +1950,7 @@ text(JSON.stringify(Object.getOwnPropertyNames(globalThis).sort()));
|
||||
"encodeURI",
|
||||
"encodeURIComponent",
|
||||
"escape",
|
||||
"exit",
|
||||
"eval",
|
||||
"globalThis",
|
||||
"image",
|
||||
|
||||
Reference in New Issue
Block a user