Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cc4f406
Make ToolOutput a trait
pakrym-oai Mar 10, 2026
ba57562
Merge remote-tracking branch 'origin/main' into pakrym/refactor-toolo…
pakrym-oai Mar 10, 2026
79dae08
Fix core build after main merge
pakrym-oai Mar 10, 2026
deefa91
Update AnyToolHandler output flow
pakrym-oai Mar 10, 2026
a3d5817
Merge remote-tracking branch 'origin/main' into pakrym/refactor-toolo…
pakrym-oai Mar 10, 2026
24a7b15
Merge branch 'pakrym/refactor-tooloutput-into-trait' into pakrym/forc…
pakrym-oai Mar 10, 2026
dcaa14a
Format tool output handler changes
pakrym-oai Mar 10, 2026
e37195b
Remove dfa_on_request artifact
pakrym-oai Mar 10, 2026
4e219f5
Update tests to use content items to
pakrym-oai Mar 10, 2026
1b9854a
Merge remote-tracking branch 'origin/main' into pakrym/force-single-o…
pakrym-oai Mar 10, 2026
23d179a
Update code_mode handler output type
pakrym-oai Mar 10, 2026
b05b826
Add ExecCommandToolOutput truncation
pakrym-oai Mar 10, 2026
3909833
Update single-text tool output tests
pakrym-oai Mar 10, 2026
b0f0503
Merge remote-tracking branch 'origin/main' into pakrym/force-single-o…
pakrym-oai Mar 10, 2026
6d531fb
Merge branch 'pakrym/force-single-output-type-for' into pakrym/locate…
pakrym-oai Mar 10, 2026
c977c4c
Merge remote-tracking branch 'origin/main' into pakrym/locate-exec-co…
pakrym-oai Mar 10, 2026
a4ad749
Add code-mode tool output results
pakrym-oai Mar 10, 2026
c639f58
Simplify tool registry dispatch
pakrym-oai Mar 10, 2026
4905115
Rename ToolOutput response method
pakrym-oai Mar 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions codex-rs/core/src/client_common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ pub(crate) mod tools {
use codex_protocol::config_types::WebSearchUserLocationType;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value;

/// When serialized as JSON, this produces a valid "Tool" in the OpenAI
/// Responses API.
Expand Down Expand Up @@ -268,6 +269,8 @@ pub(crate) mod tools {
/// `properties` must be present in `required`.
pub(crate) strict: bool,
pub(crate) parameters: JsonSchema,
#[serde(skip)]
pub(crate) output_schema: Option<Value>,
}
}

Expand Down
99 changes: 12 additions & 87 deletions codex-rs/core/src/tools/code_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,10 @@ use crate::tools::context::ToolPayload;
use crate::tools::js_repl::resolve_compatible_node;
use crate::tools::router::ToolCall;
use crate::tools::router::ToolCallSource;
use codex_protocol::models::ContentItem;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseInputItem;
use serde::Deserialize;
use serde::Serialize;
use serde_json::Value as JsonValue;
use serde_json::json;
use tokio::io::AsyncBufReadExt;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
Expand Down Expand Up @@ -60,7 +55,7 @@ enum HostToNodeMessage {
},
Response {
id: String,
content_items: Vec<JsonValue>,
code_mode_result: JsonValue,
},
}

Expand Down Expand Up @@ -90,11 +85,11 @@ pub(crate) fn instructions(config: &Config) -> Option<String> {
section.push_str("- `code_mode` is a freeform/custom tool. Direct `code_mode` calls must send raw JavaScript tool input. Do not wrap code in JSON, quotes, or markdown code fences.\n");
section.push_str("- Direct tool calls remain available while `code_mode` is enabled.\n");
section.push_str("- `code_mode` uses the same Node runtime resolution as `js_repl`. If needed, point `js_repl_node_path` at the Node binary you want Codex to use.\n");
section.push_str("- Import nested tools from `tools.js`, for example `import { exec_command } from \"tools.js\"` or `import { tools } from \"tools.js\"`. `tools[name]` and identifier wrappers like `await exec_command(args)` remain available for compatibility. Nested tool calls resolve to arrays of content items.\n");
section.push_str("- Import nested tools from `tools.js`, for example `import { exec_command } from \"tools.js\"` or `import { tools } from \"tools.js\"`. `tools[name]` and identifier wrappers like `await exec_command(args)` remain available for compatibility. Nested tool calls resolve to their code-mode result values.\n");
section.push_str(
"- Function tools require JSON object arguments. Freeform tools require raw strings.\n",
);
section.push_str("- `add_content(value)` is synchronous. It accepts a content item or an array of content items, so `add_content(await exec_command(...))` returns the same content items a direct tool call would expose to the model.\n");
section.push_str("- `add_content(value)` is synchronous. It accepts a content item, an array of content items, or a string. Structured nested-tool results should be converted to text first, for example with `JSON.stringify(...)`.\n");
section
.push_str("- Only content passed to `add_content(value)` is surfaced back to the model.");
Some(section)
Expand Down Expand Up @@ -186,7 +181,7 @@ async fn execute_node(
NodeToHostMessage::ToolCall { id, name, input } => {
let response = HostToNodeMessage::Response {
id,
content_items: call_nested_tool(exec.clone(), name, input).await,
code_mode_result: call_nested_tool(exec.clone(), name, input).await,
};
write_message(&mut stdin, &response).await?;
}
Expand Down Expand Up @@ -290,9 +285,9 @@ async fn call_nested_tool(
exec: ExecContext,
tool_name: String,
input: Option<JsonValue>,
) -> Vec<JsonValue> {
) -> JsonValue {
if tool_name == "code_mode" {
return error_content_items_json("code_mode cannot invoke itself".to_string());
return JsonValue::String("code_mode cannot invoke itself".to_string());
}

let nested_config = exec.turn.tools_config.for_code_mode_nested_tools();
Expand All @@ -306,16 +301,16 @@ async fn call_nested_tool(
let specs = router.specs();
let payload = match build_nested_tool_payload(&specs, &tool_name, input) {
Ok(payload) => payload,
Err(error) => return error_content_items_json(error),
Err(error) => return JsonValue::String(error),
};

let call = ToolCall {
tool_name: tool_name.clone(),
call_id: format!("code_mode-{}", uuid::Uuid::new_v4()),
payload,
};
let response = router
.dispatch_tool_call(
let result = router
.dispatch_tool_call_with_code_mode_result(
Arc::clone(&exec.session),
Arc::clone(&exec.turn),
Arc::clone(&exec.tracker),
Expand All @@ -324,11 +319,9 @@ async fn call_nested_tool(
)
.await;

match response {
Ok(response) => {
json_values_from_output_content_items(content_items_from_response_input(response))
}
Err(error) => error_content_items_json(error.to_string()),
match result {
Ok(result) => result.code_mode_result(),
Err(error) => JsonValue::String(error.to_string()),
}
}

Expand Down Expand Up @@ -387,70 +380,6 @@ fn build_freeform_tool_payload(
}
}

fn content_items_from_response_input(
response: ResponseInputItem,
) -> Vec<FunctionCallOutputContentItem> {
match response {
ResponseInputItem::Message { content, .. } => content
.into_iter()
.map(function_output_content_item_from_content_item)
.collect(),
ResponseInputItem::FunctionCallOutput { output, .. } => {
content_items_from_function_output(output)
}
ResponseInputItem::CustomToolCallOutput { output, .. } => {
content_items_from_function_output(output)
}
ResponseInputItem::McpToolCallOutput { result, .. } => match result {
Ok(result) => {
content_items_from_function_output(FunctionCallOutputPayload::from(&result))
}
Err(error) => vec![FunctionCallOutputContentItem::InputText { text: error }],
},
}
}

fn content_items_from_function_output(
output: FunctionCallOutputPayload,
) -> Vec<FunctionCallOutputContentItem> {
match output.body {
FunctionCallOutputBody::Text(text) => {
vec![FunctionCallOutputContentItem::InputText { text }]
}
FunctionCallOutputBody::ContentItems(items) => items,
}
}

fn function_output_content_item_from_content_item(
item: ContentItem,
) -> FunctionCallOutputContentItem {
match item {
ContentItem::InputText { text } | ContentItem::OutputText { text } => {
FunctionCallOutputContentItem::InputText { text }
}
ContentItem::InputImage { image_url } => FunctionCallOutputContentItem::InputImage {
image_url,
detail: None,
},
}
}

fn json_values_from_output_content_items(
content_items: Vec<FunctionCallOutputContentItem>,
) -> Vec<JsonValue> {
content_items
.into_iter()
.map(|item| match item {
FunctionCallOutputContentItem::InputText { text } => {
json!({ "type": "input_text", "text": text })
}
FunctionCallOutputContentItem::InputImage { image_url, detail } => {
json!({ "type": "input_image", "image_url": image_url, "detail": detail })
}
})
.collect()
}

fn output_content_items_from_json_values(
content_items: Vec<JsonValue>,
) -> Result<Vec<FunctionCallOutputContentItem>, String> {
Expand All @@ -463,7 +392,3 @@ fn output_content_items_from_json_values(
})
.collect()
}

fn error_content_items_json(message: String) -> Vec<JsonValue> {
vec![json!({ "type": "input_text", "text": message })]
}
11 changes: 9 additions & 2 deletions codex-rs/core/src/tools/code_mode_bridge.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,20 @@ function __codexCloneContentItem(item) {
}
}

function __codexNormalizeContentItems(value) {
function __codexNormalizeRawContentItems(value) {
if (Array.isArray(value)) {
return value.flatMap((entry) => __codexNormalizeContentItems(entry));
return value.flatMap((entry) => __codexNormalizeRawContentItems(entry));
}
return [__codexCloneContentItem(value)];
}

function __codexNormalizeContentItems(value) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't worry about this. It's going to change a lot.

if (typeof value === 'string') {
return [{ type: 'input_text', text: value }];
}
return __codexNormalizeRawContentItems(value);
}

Object.defineProperty(globalThis, '__codexContentItems', {
value: __codexContentItems,
configurable: true,
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/core/src/tools/code_mode_runner.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ function createProtocol() {
return;
}
pending.delete(message.id);
entry.resolve(Array.isArray(message.content_items) ? message.content_items : []);
entry.resolve(message.code_mode_result ?? '');
return;
}

Expand Down
Loading
Loading