Codex Handbook
code-mode/src/runtime/callbacks.rs 324 lines
use codex_code_mode_protocol::FunctionCallOutputContentItem;use super::EXIT_SENTINEL;use super::RuntimeEvent;use super::RuntimeState;use super::timers;use super::value::json_to_v8;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;pub(super) fn tool_callback(    scope: &mut v8::PinScope<'_, '_>,    args: v8::FunctionCallbackArguments,    mut retval: v8::ReturnValue<v8::Value>,) {    let tool_index = match args.data().to_rust_string_lossy(scope).parse::<usize>() {        Ok(tool_index) => tool_index,        Err(_) => {            throw_type_error(scope, "invalid tool callback data");            return;        }    };    let input = if args.length() == 0 {        Ok(None)    } else {        v8_value_to_json(scope, args.get(0))    };    let input = match input {        Ok(input) => input,        Err(error_text) => {            throw_type_error(scope, &error_text);            return;        }    };    let Some(resolver) = v8::PromiseResolver::new(scope) else {        throw_type_error(scope, "failed to create tool promise");        return;    };    let promise = resolver.get_promise(scope);    let resolver = v8::Global::new(scope, resolver);    let (tool_name, tool_kind) = {        let Some(state) = scope.get_slot::<RuntimeState>() else {            throw_type_error(scope, "runtime state unavailable");            return;        };        let Some(tool) = state.enabled_tools.get(tool_index) else {            throw_type_error(scope, "tool callback data is out of range");            return;        };        (tool.tool_name.clone(), tool.kind)    };    let Some(state) = scope.get_slot_mut::<RuntimeState>() else {        throw_type_error(scope, "runtime state unavailable");        return;    };    let id = format!("tool-{}", 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::ToolCall {        id,        name: tool_name,        kind: tool_kind,        input,    });    retval.set(promise.into());}pub(super) fn text_callback(    scope: &mut v8::PinScope<'_, '_>,    args: v8::FunctionCallbackArguments,    mut retval: v8::ReturnValue<v8::Value>,) {    let value = if args.length() == 0 {        v8::undefined(scope).into()    } else {        args.get(0)    };    let text = match serialize_output_text(scope, value) {        Ok(text) => text,        Err(error_text) => {            throw_type_error(scope, &error_text);            return;        }    };    if let Some(state) = scope.get_slot::<RuntimeState>() {        let _ = state.event_tx.send(RuntimeEvent::ContentItem(            FunctionCallOutputContentItem::InputText { text },        ));    }    retval.set(v8::undefined(scope).into());}pub(super) fn image_callback(    scope: &mut v8::PinScope<'_, '_>,    args: v8::FunctionCallbackArguments,    mut retval: v8::ReturnValue<v8::Value>,) {    let value = if args.length() == 0 {        v8::undefined(scope).into()    } else {        args.get(0)    };    let detail_override = if args.length() < 2 {        None    } else {        let detail = args.get(1);        if detail.is_string() {            Some(detail.to_rust_string_lossy(scope))        } else if detail.is_null() || detail.is_undefined() {            None        } else {            throw_type_error(scope, "image detail must be a string when provided");            return;        }    };    let image_item = match normalize_output_image(scope, value, detail_override) {        Ok(image_item) => image_item,        Err(()) => return,    };    if let Some(state) = scope.get_slot::<RuntimeState>() {        let _ = state.event_tx.send(RuntimeEvent::ContentItem(image_item));    }    retval.set(v8::undefined(scope).into());}pub(super) fn generated_image_callback(    scope: &mut v8::PinScope<'_, '_>,    args: v8::FunctionCallbackArguments,    mut retval: v8::ReturnValue<v8::Value>,) {    let value = if args.length() == 0 {        v8::undefined(scope).into()    } else {        args.get(0)    };    let output_hint = match generated_image_output_hint(scope, value) {        Ok(output_hint) => output_hint,        Err(error_text) => {            throw_type_error(scope, &error_text);            return;        }    };    let image_item = match normalize_output_image(scope, value, /*detail_override*/ None) {        Ok(image_item) => image_item,        Err(()) => return,    };    if let Some(state) = scope.get_slot::<RuntimeState>() {        let _ = state.event_tx.send(RuntimeEvent::ContentItem(image_item));        if let Some(text) = output_hint {            let _ = state.event_tx.send(RuntimeEvent::ContentItem(                FunctionCallOutputContentItem::InputText { text },            ));        }    }    retval.set(v8::undefined(scope).into());}fn generated_image_output_hint(    scope: &mut v8::PinScope<'_, '_>,    value: v8::Local<'_, v8::Value>,) -> Result<Option<String>, String> {    let object = v8::Local::<v8::Object>::try_from(value)        .map_err(|_| "generatedImage expects an image generation result object".to_string())?;    let key = v8::String::new(scope, "output_hint")        .ok_or_else(|| "failed to allocate generatedImage helper keys".to_string())?;    let output_hint = object        .get(scope, key.into())        .ok_or_else(|| "failed to read generatedImage output_hint".to_string())?;    if output_hint.is_undefined() {        return Ok(None);    }    if !output_hint.is_string() {        return Err("generatedImage output_hint must be a string when provided".to_string());    }    Ok(Some(output_hint.to_rust_string_lossy(scope)))}pub(super) fn store_callback(    scope: &mut v8::PinScope<'_, '_>,    args: v8::FunctionCallbackArguments,    _retval: v8::ReturnValue<v8::Value>,) {    let key = match args.get(0).to_string(scope) {        Some(key) => key.to_rust_string_lossy(scope),        None => {            throw_type_error(scope, "store key must be a string");            return;        }    };    let value = args.get(1);    let serialized = match v8_value_to_json(scope, value) {        Ok(Some(value)) => value,        Ok(None) => {            throw_type_error(                scope,                &format!("Unable to store {key:?}. Only plain serializable objects can be stored."),            );            return;        }        Err(error_text) => {            throw_type_error(scope, &error_text);            return;        }    };    if let Some(state) = scope.get_slot_mut::<RuntimeState>() {        state.stored_values.insert(key.clone(), serialized.clone());        state.stored_value_writes.insert(key, serialized);    }}pub(super) fn load_callback(    scope: &mut v8::PinScope<'_, '_>,    args: v8::FunctionCallbackArguments,    mut retval: v8::ReturnValue<v8::Value>,) {    let key = match args.get(0).to_string(scope) {        Some(key) => key.to_rust_string_lossy(scope),        None => {            throw_type_error(scope, "load key must be a string");            return;        }    };    let value = scope        .get_slot::<RuntimeState>()        .and_then(|state| state.stored_values.get(&key))        .cloned();    let Some(value) = value else {        retval.set(v8::undefined(scope).into());        return;    };    let Some(value) = json_to_v8(scope, &value) else {        throw_type_error(scope, "failed to load stored value");        return;    };    retval.set(value);}pub(super) fn notify_callback(    scope: &mut v8::PinScope<'_, '_>,    args: v8::FunctionCallbackArguments,    mut retval: v8::ReturnValue<v8::Value>,) {    let value = if args.length() == 0 {        v8::undefined(scope).into()    } else {        args.get(0)    };    let text = match serialize_output_text(scope, value) {        Ok(text) => text,        Err(error_text) => {            throw_type_error(scope, &error_text);            return;        }    };    if text.trim().is_empty() {        throw_type_error(scope, "notify expects non-empty text");        return;    }    if let Some(state) = scope.get_slot::<RuntimeState>() {        let _ = state.event_tx.send(RuntimeEvent::Notify {            call_id: state.tool_call_id.clone(),            text,        });    }    retval.set(v8::undefined(scope).into());}pub(super) fn set_timeout_callback(    scope: &mut v8::PinScope<'_, '_>,    args: v8::FunctionCallbackArguments,    mut retval: v8::ReturnValue<v8::Value>,) {    let timeout_id = match timers::schedule_timeout(scope, args) {        Ok(timeout_id) => timeout_id,        Err(error_text) => {            throw_type_error(scope, &error_text);            return;        }    };    retval.set(v8::Number::new(scope, timeout_id as f64).into());}pub(super) fn clear_timeout_callback(    scope: &mut v8::PinScope<'_, '_>,    args: v8::FunctionCallbackArguments,    mut retval: v8::ReturnValue<v8::Value>,) {    if let Err(error_text) = timers::clear_timeout(scope, args) {        throw_type_error(scope, &error_text);        return;    }    retval.set(v8::undefined(scope).into());}pub(super) fn yield_control_callback(    scope: &mut v8::PinScope<'_, '_>,    _args: v8::FunctionCallbackArguments,    _retval: v8::ReturnValue<v8::Value>,) {    if let Some(state) = scope.get_slot::<RuntimeState>() {        let _ = state.event_tx.send(RuntimeEvent::YieldRequested);    }}pub(super) fn exit_callback(    scope: &mut v8::PinScope<'_, '_>,    _args: v8::FunctionCallbackArguments,    _retval: v8::ReturnValue<v8::Value>,) {    if let Some(state) = scope.get_slot_mut::<RuntimeState>() {        state.exit_requested = true;    }    if let Some(error) = v8::String::new(scope, EXIT_SENTINEL) {        scope.throw_exception(error.into());    }}