mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
feat(code-mode): add io.write helper
Expose a host-mediated io.write primitive in Code Mode that writes serializable text or bytes to env://current paths via the core harness. Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
co-authored by
Codex
parent
25c7e752f8
commit
f1815041e4
@@ -30,6 +30,7 @@ const EXEC_DESCRIPTION_TEMPLATE: &str = r#"Run JavaScript code to orchestrate/co
|
||||
- `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.
|
||||
- `notify(value: string | number | boolean | undefined | null)`: immediately injects an extra `custom_tool_call_output` for the current `exec` call. Values are stringified like `text(...)`.
|
||||
- `io.write(value: string | number[] | TextContent | BlobResourceContents, destination: "env://current/<relative-path>")`: writes text or bytes to the current environment filesystem and returns `{ uri, path, bytes_written }`.
|
||||
- `setTimeout(callback: () => void, delayMs?: number)`: schedules a callback to run later and returns a timeout id. Pending timeouts do not keep `exec` alive by themselves; await an explicit promise if you need to wait for one.
|
||||
- `clearTimeout(timeoutId?: number)`: cancels a timeout created by `setTimeout`.
|
||||
- `ALL_TOOLS`: metadata for the enabled nested tools as `{ name, description }` entries.
|
||||
|
||||
@@ -18,6 +18,7 @@ pub use description::render_json_schema_to_typescript;
|
||||
pub use response::DEFAULT_IMAGE_DETAIL;
|
||||
pub use response::FunctionCallOutputContentItem;
|
||||
pub use response::ImageDetail;
|
||||
pub use runtime::CodeModeIoWrite;
|
||||
pub use runtime::CodeModeNestedToolCall;
|
||||
pub use runtime::DEFAULT_EXEC_YIELD_TIME_MS;
|
||||
pub use runtime::DEFAULT_MAX_OUTPUT_TOKENS_PER_EXEC_CALL;
|
||||
|
||||
@@ -9,6 +9,7 @@ use super::value::normalize_output_image;
|
||||
use super::value::serialize_output_text;
|
||||
use super::value::throw_type_error;
|
||||
use super::value::v8_value_to_json;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
pub(super) fn tool_callback(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
@@ -74,6 +75,59 @@ pub(super) fn tool_callback(
|
||||
retval.set(promise.into());
|
||||
}
|
||||
|
||||
pub(super) fn io_write_callback(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
args: v8::FunctionCallbackArguments,
|
||||
mut retval: v8::ReturnValue<v8::Value>,
|
||||
) {
|
||||
if args.length() < 2 {
|
||||
throw_type_error(scope, "io.write expects a value and destination");
|
||||
return;
|
||||
}
|
||||
|
||||
let value = match v8_value_to_json(scope, args.get(0)) {
|
||||
Ok(Some(value)) => value,
|
||||
Ok(None) => JsonValue::Null,
|
||||
Err(error_text) => {
|
||||
throw_type_error(scope, &error_text);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let destination = match args.get(1).to_string(scope) {
|
||||
Some(destination) => destination.to_rust_string_lossy(scope),
|
||||
None => {
|
||||
throw_type_error(scope, "io.write destination must be a string");
|
||||
return;
|
||||
}
|
||||
};
|
||||
if destination.trim().is_empty() {
|
||||
throw_type_error(scope, "io.write destination must be non-empty");
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(resolver) = v8::PromiseResolver::new(scope) else {
|
||||
throw_type_error(scope, "failed to create io.write promise");
|
||||
return;
|
||||
};
|
||||
let promise = resolver.get_promise(scope);
|
||||
let resolver = v8::Global::new(scope, resolver);
|
||||
|
||||
let Some(state) = scope.get_slot_mut::<RuntimeState>() else {
|
||||
throw_type_error(scope, "runtime state unavailable");
|
||||
return;
|
||||
};
|
||||
let id = format!("io-write-{}", state.next_tool_call_id);
|
||||
state.next_tool_call_id = state.next_tool_call_id.saturating_add(1);
|
||||
let event_tx = state.event_tx.clone();
|
||||
state.pending_tool_calls.insert(id.clone(), resolver);
|
||||
let _ = event_tx.send(RuntimeEvent::IoWrite {
|
||||
id,
|
||||
value,
|
||||
destination,
|
||||
});
|
||||
retval.set(promise.into());
|
||||
}
|
||||
|
||||
pub(super) fn text_callback(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
args: v8::FunctionCallbackArguments,
|
||||
|
||||
@@ -2,6 +2,7 @@ use super::RuntimeState;
|
||||
use super::callbacks::clear_timeout_callback;
|
||||
use super::callbacks::exit_callback;
|
||||
use super::callbacks::image_callback;
|
||||
use super::callbacks::io_write_callback;
|
||||
use super::callbacks::load_callback;
|
||||
use super::callbacks::notify_callback;
|
||||
use super::callbacks::set_timeout_callback;
|
||||
@@ -19,6 +20,7 @@ pub(super) fn install_globals(scope: &mut v8::PinScope<'_, '_>) -> Result<(), St
|
||||
|
||||
let tools = build_tools_object(scope)?;
|
||||
let all_tools = build_all_tools_value(scope)?;
|
||||
let io = build_io_object(scope)?;
|
||||
let clear_timeout = helper_function(scope, "clearTimeout", clear_timeout_callback)?;
|
||||
let set_timeout = helper_function(scope, "setTimeout", set_timeout_callback)?;
|
||||
let text = helper_function(scope, "text", text_callback)?;
|
||||
@@ -30,6 +32,7 @@ pub(super) fn install_globals(scope: &mut v8::PinScope<'_, '_>) -> Result<(), St
|
||||
let exit = helper_function(scope, "exit", exit_callback)?;
|
||||
|
||||
set_global(scope, global, "tools", tools.into())?;
|
||||
set_global(scope, global, "io", io.into())?;
|
||||
set_global(scope, global, "ALL_TOOLS", all_tools)?;
|
||||
set_global(scope, global, "clearTimeout", clear_timeout.into())?;
|
||||
set_global(scope, global, "setTimeout", set_timeout.into())?;
|
||||
@@ -43,6 +46,20 @@ pub(super) fn install_globals(scope: &mut v8::PinScope<'_, '_>) -> Result<(), St
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_io_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
) -> Result<v8::Local<'s, v8::Object>, String> {
|
||||
let io = v8::Object::new(scope);
|
||||
let write_key = v8::String::new(scope, "write")
|
||||
.ok_or_else(|| "failed to allocate io.write key".to_string())?;
|
||||
let write = helper_function(scope, "io.write", io_write_callback)?;
|
||||
if io.set(scope, write_key.into(), write.into()) == Some(true) {
|
||||
Ok(io)
|
||||
} else {
|
||||
Err("failed to set io.write".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
fn build_tools_object<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
) -> Result<v8::Local<'s, v8::Object>, String> {
|
||||
|
||||
@@ -100,9 +100,18 @@ pub struct CodeModeNestedToolCall {
|
||||
pub input: Option<JsonValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CodeModeIoWrite {
|
||||
pub cell_id: String,
|
||||
pub runtime_write_id: String,
|
||||
pub value: JsonValue,
|
||||
pub destination: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum TurnMessage {
|
||||
ToolCall(CodeModeNestedToolCall),
|
||||
IoWrite(CodeModeIoWrite),
|
||||
Notify {
|
||||
cell_id: String,
|
||||
call_id: String,
|
||||
@@ -128,6 +137,11 @@ pub(crate) enum RuntimeEvent {
|
||||
name: ToolName,
|
||||
input: Option<JsonValue>,
|
||||
},
|
||||
IoWrite {
|
||||
id: String,
|
||||
value: JsonValue,
|
||||
destination: String,
|
||||
},
|
||||
Notify {
|
||||
call_id: String,
|
||||
text: String,
|
||||
|
||||
@@ -13,6 +13,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use tracing::warn;
|
||||
|
||||
use crate::FunctionCallOutputContentItem;
|
||||
use crate::runtime::CodeModeIoWrite;
|
||||
use crate::runtime::CodeModeNestedToolCall;
|
||||
use crate::runtime::DEFAULT_EXEC_YIELD_TIME_MS;
|
||||
use crate::runtime::ExecuteRequest;
|
||||
@@ -33,6 +34,12 @@ pub trait CodeModeTurnHost: Send + Sync {
|
||||
) -> Result<JsonValue, String>;
|
||||
|
||||
async fn notify(&self, call_id: String, cell_id: String, text: String) -> Result<(), String>;
|
||||
|
||||
async fn write_file(
|
||||
&self,
|
||||
request: CodeModeIoWrite,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> Result<JsonValue, String>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -222,6 +229,35 @@ impl CodeModeService {
|
||||
let _ = runtime_tx.send(command);
|
||||
});
|
||||
}
|
||||
TurnMessage::IoWrite(request) => {
|
||||
let host = Arc::clone(&host);
|
||||
let inner = Arc::clone(&inner);
|
||||
tokio::spawn(async move {
|
||||
let cell_id = request.cell_id.clone();
|
||||
let runtime_write_id = request.runtime_write_id.clone();
|
||||
let response = host.write_file(request, CancellationToken::new()).await;
|
||||
let runtime_tx = inner
|
||||
.sessions
|
||||
.lock()
|
||||
.await
|
||||
.get(&cell_id)
|
||||
.map(|handle| handle.runtime_tx.clone());
|
||||
let Some(runtime_tx) = runtime_tx else {
|
||||
return;
|
||||
};
|
||||
let command = match response {
|
||||
Ok(result) => RuntimeCommand::ToolResponse {
|
||||
id: runtime_write_id,
|
||||
result,
|
||||
},
|
||||
Err(error_text) => RuntimeCommand::ToolError {
|
||||
id: runtime_write_id,
|
||||
error_text,
|
||||
},
|
||||
};
|
||||
let _ = runtime_tx.send(command);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -397,6 +433,19 @@ async fn run_session_control(
|
||||
.send(TurnMessage::ToolCall(tool_call))
|
||||
.await;
|
||||
}
|
||||
RuntimeEvent::IoWrite {
|
||||
id,
|
||||
value,
|
||||
destination,
|
||||
} => {
|
||||
let request = CodeModeIoWrite {
|
||||
cell_id: cell_id.clone(),
|
||||
runtime_write_id: id,
|
||||
value,
|
||||
destination,
|
||||
};
|
||||
let _ = inner.turn_message_tx.send(TurnMessage::IoWrite(request)).await;
|
||||
}
|
||||
RuntimeEvent::Result {
|
||||
stored_values,
|
||||
error_text,
|
||||
|
||||
Reference in New Issue
Block a user