mirror of
https://github.com/openai/codex.git
synced 2026-05-19 18:52:57 +00:00
## Why Code mode only used nested spec lookup at execution time to rediscover whether a nested tool should be invoked as a function tool or a freeform tool. That information is already present in the enabled tool metadata that code mode builds to expose `tools.*` and `ALL_TOOLS`, so re-looking it up from the router was redundant and kept execution coupled to a separate spec lookup path. ## What Changed - thread `CodeModeToolKind` through the code-mode runtime `ToolCall` event and `CodeModeNestedToolCall` - emit the nested tool kind directly from the V8 callback using the already-enabled tool metadata - build nested tool payloads from the propagated kind instead of calling `find_spec` - remove the now-unused `find_spec` plumbing from the router and parallel runtime helpers - add unit coverage for function vs freeform payload shaping and update affected router tests ## Testing - `cargo test -p codex-code-mode` - `cargo test -p codex-core code_mode::tests` - `cargo test -p codex-core extension_tool_bundles_are_model_visible_and_dispatchable` - `cargo test -p codex-core model_visible_specs_filter_deferred_dynamic_tools`
272 lines
7.9 KiB
Rust
272 lines
7.9 KiB
Rust
use crate::response::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 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, 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());
|
|
}
|
|
}
|