Refactor tool output into trait implementations (#14152)

First state to making tool outputs strongly typed (and `renderable`).
This commit is contained in:
pakrym-oai 2026-03-09 20:38:32 -06:00 committed by GitHub
parent a5af11211a
commit aa04ea6bd7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 354 additions and 344 deletions

View file

@ -2851,6 +2851,7 @@ impl Session {
AskForApproval::Never => {
return Some(RequestPermissionsResponse {
permissions: PermissionProfile::default(),
scope: PermissionGrantScope::Turn,
});
}
AskForApproval::Reject(reject_config)
@ -2858,6 +2859,7 @@ impl Session {
{
return Some(RequestPermissionsResponse {
permissions: PermissionProfile::default(),
scope: PermissionGrantScope::Turn,
});
}
AskForApproval::OnFailure

View file

@ -46,8 +46,8 @@ use crate::state::TaskKind;
use crate::tasks::SessionTask;
use crate::tasks::SessionTaskContext;
use crate::tools::ToolRouter;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::ShellHandler;
use crate::tools::handlers::UnifiedExecHandler;
@ -89,6 +89,13 @@ use std::time::Duration as StdDuration;
#[path = "codex_tests_guardian.rs"]
mod guardian_tests;
fn expect_text_tool_output(output: &dyn std::any::Any) -> String {
let Some(output) = output.downcast_ref::<TextToolOutput>() else {
panic!("unexpected tool output");
};
output.text.clone()
}
struct InstructionsTestCase {
slug: &'static str,
expects_apply_patch_instructions: bool,
@ -2293,6 +2300,7 @@ async fn request_permissions_emits_event_when_reject_policy_allows_requests() {
}),
..Default::default()
},
scope: codex_protocol::request_permissions::PermissionGrantScope::Turn,
};
let handle = tokio::spawn({
@ -2377,6 +2385,7 @@ async fn request_permissions_returns_empty_grant_when_reject_policy_blocks_reque
Some(
codex_protocol::request_permissions::RequestPermissionsResponse {
permissions: codex_protocol::models::PermissionProfile::default(),
scope: codex_protocol::request_permissions::PermissionGrantScope::Turn,
}
)
);
@ -4133,13 +4142,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
})
.await;
let output = match resp2.expect("expected Ok result") {
ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
..
} => content,
_ => panic!("unexpected tool output"),
};
let output = expect_text_tool_output(&*resp2.expect("expected Ok result"));
#[derive(Deserialize, PartialEq, Eq, Debug)]
struct ResponseExecMetadata {

View file

@ -8,12 +8,12 @@ use crate::features::Feature;
use crate::guardian::GUARDIAN_SUBAGENT_NAME;
use crate::protocol::AskForApproval;
use crate::sandboxing::SandboxPermissions;
use crate::tools::context::TextToolOutput;
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_app_server_protocol::ConfigLayerSource;
use codex_execpolicy::Decision;
use codex_execpolicy::Evaluation;
use codex_execpolicy::RuleMatch;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::NetworkPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::FileSystemSandboxPolicy;
@ -33,6 +33,13 @@ use std::fs;
use std::sync::Arc;
use tempfile::tempdir;
fn expect_text_output(output: &dyn std::any::Any) -> String {
let Some(output) = output.downcast_ref::<TextToolOutput>() else {
panic!("unexpected tool output");
};
output.text.clone()
}
#[tokio::test]
async fn guardian_allows_shell_additional_permissions_requests_past_policy_validation() {
let server = start_mock_server().await;
@ -152,13 +159,7 @@ async fn guardian_allows_shell_additional_permissions_requests_past_policy_valid
})
.await;
let output = match resp.expect("expected Ok result") {
ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
..
} => content,
_ => panic!("unexpected tool output"),
};
let output = expect_text_output(&*resp.expect("expected Ok result"));
#[derive(Deserialize, PartialEq, Eq, Debug)]
struct ResponseExecMetadata {

View file

@ -6,10 +6,12 @@ use crate::tools::TELEMETRY_PREVIEW_TRUNCATION_NOTICE;
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_protocol::mcp::CallToolResult;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::FunctionCallOutputPayload;
use codex_protocol::models::ResponseInputItem;
use codex_protocol::models::ShellToolCallParams;
use codex_utils_string::take_bytes_at_char_boundary;
use std::any::Any;
use std::borrow::Cow;
use std::sync::Arc;
use tokio::sync::Mutex;
@ -61,65 +63,111 @@ impl ToolPayload {
}
}
#[derive(Clone)]
pub enum ToolOutput {
Function {
// Canonical output body for function-style tools. This may be plain text
// or structured content items.
body: FunctionCallOutputBody,
success: Option<bool>,
},
Mcp {
result: Result<CallToolResult, String>,
},
pub trait ToolOutput: Any + Send {
fn log_preview(&self) -> String;
fn success_for_logging(&self) -> bool;
fn into_response(self: Box<Self>, call_id: &str, payload: &ToolPayload) -> ResponseInputItem;
}
impl ToolOutput {
pub fn log_preview(&self) -> String {
match self {
ToolOutput::Function { body, .. } => {
telemetry_preview(&body.to_text().unwrap_or_default())
}
ToolOutput::Mcp { result } => format!("{result:?}"),
}
pub type ToolOutputBox = Box<dyn ToolOutput>;
pub struct McpToolOutput {
pub result: Result<CallToolResult, String>,
}
impl ToolOutput for McpToolOutput {
fn log_preview(&self) -> String {
format!("{:?}", self.result)
}
pub fn success_for_logging(&self) -> bool {
match self {
ToolOutput::Function { success, .. } => success.unwrap_or(true),
ToolOutput::Mcp { result } => result.is_ok(),
}
fn success_for_logging(&self) -> bool {
self.result.is_ok()
}
pub fn into_response(self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem {
match self {
ToolOutput::Function { body, success } => {
// `custom_tool_call` is the Responses API item type for freeform
// tools (`ToolSpec::Freeform`, e.g. freeform `apply_patch` or
// `js_repl`).
if matches!(payload, ToolPayload::Custom { .. }) {
return ResponseInputItem::CustomToolCallOutput {
call_id: call_id.to_string(),
output: FunctionCallOutputPayload { body, success },
};
}
// Function-style outputs (JSON function tools, including dynamic
// tools and MCP adaptation) preserve the exact body shape.
ResponseInputItem::FunctionCallOutput {
call_id: call_id.to_string(),
output: FunctionCallOutputPayload { body, success },
}
}
// Direct MCP response path for MCP tool result envelopes.
ToolOutput::Mcp { result } => ResponseInputItem::McpToolCallOutput {
call_id: call_id.to_string(),
result,
},
fn into_response(self: Box<Self>, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem {
let Self { result } = *self;
ResponseInputItem::McpToolCallOutput {
call_id: call_id.to_string(),
result,
}
}
}
pub struct TextToolOutput {
pub text: String,
pub success: Option<bool>,
}
impl ToolOutput for TextToolOutput {
fn log_preview(&self) -> String {
telemetry_preview(&self.text)
}
fn success_for_logging(&self) -> bool {
self.success.unwrap_or(true)
}
fn into_response(self: Box<Self>, call_id: &str, payload: &ToolPayload) -> ResponseInputItem {
let Self { text, success } = *self;
function_tool_response(
call_id,
payload,
FunctionCallOutputBody::Text(text),
success,
)
}
}
pub struct ContentToolOutput {
pub content: Vec<FunctionCallOutputContentItem>,
pub success: Option<bool>,
}
impl ToolOutput for ContentToolOutput {
fn log_preview(&self) -> String {
telemetry_preview(
&FunctionCallOutputBody::ContentItems(self.content.clone())
.to_text()
.unwrap_or_default(),
)
}
fn success_for_logging(&self) -> bool {
self.success.unwrap_or(true)
}
fn into_response(self: Box<Self>, call_id: &str, payload: &ToolPayload) -> ResponseInputItem {
let Self { content, success } = *self;
function_tool_response(
call_id,
payload,
FunctionCallOutputBody::ContentItems(content),
success,
)
}
}
fn function_tool_response(
call_id: &str,
payload: &ToolPayload,
body: FunctionCallOutputBody,
success: Option<bool>,
) -> ResponseInputItem {
if matches!(payload, ToolPayload::Custom { .. }) {
return ResponseInputItem::CustomToolCallOutput {
call_id: call_id.to_string(),
output: FunctionCallOutputPayload { body, success },
};
}
ResponseInputItem::FunctionCallOutput {
call_id: call_id.to_string(),
output: FunctionCallOutputPayload { body, success },
}
}
fn telemetry_preview(content: &str) -> String {
let truncated_slice = take_bytes_at_char_boundary(content, TELEMETRY_PREVIEW_MAX_BYTES);
let truncated_by_bytes = truncated_slice.len() < content.len();
@ -163,7 +211,6 @@ fn telemetry_preview(content: &str) -> String {
#[cfg(test)]
mod tests {
use super::*;
use codex_protocol::models::FunctionCallOutputContentItem;
use pretty_assertions::assert_eq;
#[test]
@ -171,10 +218,10 @@ mod tests {
let payload = ToolPayload::Custom {
input: "patch".to_string(),
};
let response = ToolOutput::Function {
body: FunctionCallOutputBody::Text("patched".to_string()),
let response = Box::new(TextToolOutput {
text: "patched".to_string(),
success: Some(true),
}
})
.into_response("call-42", &payload);
match response {
@ -193,10 +240,10 @@ mod tests {
let payload = ToolPayload::Function {
arguments: "{}".to_string(),
};
let response = ToolOutput::Function {
body: FunctionCallOutputBody::Text("ok".to_string()),
let response = Box::new(TextToolOutput {
text: "ok".to_string(),
success: Some(true),
}
})
.into_response("fn-1", &payload);
match response {
@ -215,8 +262,8 @@ mod tests {
let payload = ToolPayload::Custom {
input: "patch".to_string(),
};
let response = ToolOutput::Function {
body: FunctionCallOutputBody::ContentItems(vec![
let response = Box::new(ContentToolOutput {
content: vec![
FunctionCallOutputContentItem::InputText {
text: "line 1".to_string(),
},
@ -227,9 +274,9 @@ mod tests {
FunctionCallOutputContentItem::InputText {
text: "line 2".to_string(),
},
]),
],
success: Some(true),
}
})
.into_response("call-99", &payload);
match response {
@ -257,12 +304,10 @@ mod tests {
#[test]
fn log_preview_uses_content_items_when_plain_text_is_missing() {
let output = ToolOutput::Function {
body: FunctionCallOutputBody::ContentItems(vec![
FunctionCallOutputContentItem::InputText {
text: "preview".to_string(),
},
]),
let output = ContentToolOutput {
content: vec![FunctionCallOutputContentItem::InputText {
text: "preview".to_string(),
}],
success: Some(true),
};

View file

@ -6,8 +6,9 @@ use crate::codex::TurnContext;
use crate::config::Config;
use crate::error::CodexErr;
use crate::function_tool::FunctionCallError;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::multi_agents::build_agent_spawn_config;
use crate::tools::handlers::parse_arguments;
@ -15,7 +16,6 @@ use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use async_trait::async_trait;
use codex_protocol::ThreadId;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::protocol::SessionSource;
use codex_protocol::protocol::SubAgentSource;
use codex_protocol::user_input::UserInput;
@ -183,7 +183,7 @@ impl ToolHandler for BatchJobHandler {
matches!(payload, ToolPayload::Function { .. })
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation {
session,
turn,
@ -223,7 +223,7 @@ mod spawn_agents_on_csv {
session: Arc<Session>,
turn: Arc<TurnContext>,
arguments: String,
) -> Result<ToolOutput, FunctionCallError> {
) -> Result<ToolOutputBox, FunctionCallError> {
let args: SpawnAgentsOnCsvArgs = parse_arguments(arguments.as_str())?;
if args.instruction.trim().is_empty() {
return Err(FunctionCallError::RespondToModel(
@ -456,10 +456,10 @@ mod spawn_agents_on_csv {
"failed to serialize spawn_agents_on_csv result: {err}"
))
})?;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
})
}))
}
}
@ -469,7 +469,7 @@ mod report_agent_job_result {
pub async fn handle(
session: Arc<Session>,
arguments: String,
) -> Result<ToolOutput, FunctionCallError> {
) -> Result<ToolOutputBox, FunctionCallError> {
let args: ReportAgentJobResultArgs = parse_arguments(arguments.as_str())?;
if !args.result.is_object() {
return Err(FunctionCallError::RespondToModel(
@ -505,10 +505,10 @@ mod report_agent_job_result {
"failed to serialize report_agent_job_result result: {err}"
))
})?;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
})
}))
}
}

View file

@ -1,4 +1,3 @@
use codex_protocol::models::FunctionCallOutputBody;
use std::collections::BTreeMap;
use std::path::Path;
@ -13,8 +12,9 @@ use crate::codex::Session;
use crate::codex::TurnContext;
use crate::function_tool::FunctionCallError;
use crate::tools::context::SharedTurnDiffTracker;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::events::ToolEmitter;
use crate::tools::events::ToolEventCtx;
@ -107,7 +107,7 @@ impl ToolHandler for ApplyPatchHandler {
true
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation {
session,
turn,
@ -140,10 +140,10 @@ impl ToolHandler for ApplyPatchHandler {
match apply_patch::apply_patch(turn.as_ref(), changes).await {
InternalApplyPatchInvocation::Output(item) => {
let content = item?;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
})
}))
}
InternalApplyPatchInvocation::DelegateToExec(apply) => {
let changes = convert_apply_patch_to_protocol(&apply.action);
@ -204,10 +204,10 @@ impl ToolHandler for ApplyPatchHandler {
Some(&tracker),
);
let content = emitter.finish(event_ctx, out).await?;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
})
}))
}
}
}
@ -241,7 +241,7 @@ pub(crate) async fn intercept_apply_patch(
tracker: Option<&SharedTurnDiffTracker>,
call_id: &str,
tool_name: &str,
) -> Result<Option<ToolOutput>, FunctionCallError> {
) -> Result<Option<ToolOutputBox>, FunctionCallError> {
match codex_apply_patch::maybe_parse_apply_patch_verified(command, cwd) {
codex_apply_patch::MaybeApplyPatchVerified::Body(changes) => {
session
@ -255,10 +255,10 @@ pub(crate) async fn intercept_apply_patch(
match apply_patch::apply_patch(turn.as_ref(), changes).await {
InternalApplyPatchInvocation::Output(item) => {
let content = item?;
Ok(Some(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Some(Box::new(TextToolOutput {
text: content,
success: Some(true),
}))
})))
}
InternalApplyPatchInvocation::DelegateToExec(apply) => {
let changes = convert_apply_patch_to_protocol(&apply.action);
@ -317,10 +317,10 @@ pub(crate) async fn intercept_apply_patch(
tracker.as_ref().copied(),
);
let content = emitter.finish(event_ctx, out).await?;
Ok(Some(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Some(Box::new(TextToolOutput {
text: content,
success: Some(true),
}))
})))
}
}
}

View file

@ -16,8 +16,9 @@ use crate::exec::StreamOutput;
use crate::features::Feature;
use crate::function_tool::FunctionCallError;
use crate::protocol::ExecCommandSource;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::events::ToolEmitter;
use crate::tools::events::ToolEventCtx;
@ -25,7 +26,6 @@ use crate::tools::events::ToolEventFailure;
use crate::tools::events::ToolEventStage;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::models::FunctionCallOutputBody;
const ARTIFACTS_TOOL_NAME: &str = "artifacts";
const ARTIFACTS_PRAGMA_PREFIXES: [&str; 2] = ["// codex-artifacts:", "// codex-artifact-tool:"];
@ -54,7 +54,7 @@ impl ToolHandler for ArtifactsHandler {
true
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation {
session,
turn,
@ -112,10 +112,10 @@ impl ToolHandler for ArtifactsHandler {
)
.await;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(format_artifact_output(&output)),
Ok(Box::new(TextToolOutput {
text: format_artifact_output(&output),
success: Some(success),
})
}))
}
}

View file

@ -1,8 +1,9 @@
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::function_tool::FunctionCallError;
use crate::tools::context::ContentToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
@ -10,7 +11,6 @@ use crate::tools::registry::ToolKind;
use async_trait::async_trait;
use codex_protocol::dynamic_tools::DynamicToolCallRequest;
use codex_protocol::dynamic_tools::DynamicToolResponse;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::protocol::DynamicToolCallResponseEvent;
use codex_protocol::protocol::EventMsg;
@ -31,7 +31,7 @@ impl ToolHandler for DynamicToolHandler {
true
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation {
session,
turn,
@ -67,12 +67,10 @@ impl ToolHandler for DynamicToolHandler {
.into_iter()
.map(FunctionCallOutputContentItem::from)
.collect::<Vec<_>>();
let body = FunctionCallOutputBody::ContentItems(body);
Ok(ToolOutput::Function {
body,
Ok(Box::new(ContentToolOutput {
content: body,
success: Some(success),
})
}))
}
}

View file

@ -1,4 +1,3 @@
use codex_protocol::models::FunctionCallOutputBody;
use std::path::Path;
use std::time::Duration;
@ -8,8 +7,9 @@ use tokio::process::Command;
use tokio::time::timeout;
use crate::function_tool::FunctionCallError;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
@ -42,7 +42,7 @@ impl ToolHandler for GrepFilesHandler {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation { payload, turn, .. } = invocation;
let arguments = match payload {
@ -86,15 +86,15 @@ impl ToolHandler for GrepFilesHandler {
run_rg_search(pattern, include.as_deref(), &search_path, limit, &turn.cwd).await?;
if search_results.is_empty() {
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text("No matches found.".to_string()),
Ok(Box::new(TextToolOutput {
text: "No matches found.".to_string(),
success: Some(false),
})
}))
} else {
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(search_results.join("\n")),
Ok(Box::new(TextToolOutput {
text: search_results.join("\n"),
success: Some(true),
})
}))
}
}
}

View file

@ -9,8 +9,10 @@ use crate::exec::StreamOutput;
use crate::features::Feature;
use crate::function_tool::FunctionCallError;
use crate::protocol::ExecCommandSource;
use crate::tools::context::ContentToolOutput;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::events::ToolEmitter;
use crate::tools::events::ToolEventCtx;
@ -21,7 +23,6 @@ use crate::tools::js_repl::JS_REPL_PRAGMA_PREFIX;
use crate::tools::js_repl::JsReplArgs;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
pub struct JsReplHandler;
@ -106,7 +107,7 @@ impl ToolHandler for JsReplHandler {
)
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation {
session,
turn,
@ -173,14 +174,17 @@ impl ToolHandler for JsReplHandler {
)
.await;
Ok(ToolOutput::Function {
body: if items.is_empty() {
FunctionCallOutputBody::Text(content)
} else {
FunctionCallOutputBody::ContentItems(items)
},
success: Some(true),
})
if items.is_empty() {
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
}))
} else {
Ok(Box::new(ContentToolOutput {
content: items,
success: Some(true),
}))
}
}
}
@ -190,7 +194,7 @@ impl ToolHandler for JsReplResetHandler {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
if !invocation.session.features().enabled(Feature::JsRepl) {
return Err(FunctionCallError::RespondToModel(
"js_repl is disabled by feature flag".to_string(),
@ -198,10 +202,10 @@ impl ToolHandler for JsReplResetHandler {
}
let manager = invocation.turn.js_repl.manager().await?;
manager.reset().await?;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text("js_repl kernel reset".to_string()),
Ok(Box::new(TextToolOutput {
text: "js_repl kernel reset".to_string(),
success: Some(true),
})
}))
}
}

View file

@ -1,4 +1,3 @@
use codex_protocol::models::FunctionCallOutputBody;
use std::collections::VecDeque;
use std::ffi::OsStr;
use std::fs::FileType;
@ -11,8 +10,9 @@ use serde::Deserialize;
use tokio::fs;
use crate::function_tool::FunctionCallError;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
@ -52,7 +52,7 @@ impl ToolHandler for ListDirHandler {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation { payload, .. } = invocation;
let arguments = match payload {
@ -102,10 +102,10 @@ impl ToolHandler for ListDirHandler {
let mut output = Vec::with_capacity(entries.len() + 1);
output.push(format!("Absolute path: {}", path.display()));
output.extend(entries);
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(output.join("\n")),
Ok(Box::new(TextToolOutput {
text: output.join("\n"),
success: Some(true),
})
}))
}
}

View file

@ -3,8 +3,11 @@ use std::sync::Arc;
use crate::function_tool::FunctionCallError;
use crate::mcp_tool_call::handle_mcp_tool_call;
use crate::tools::context::ContentToolOutput;
use crate::tools::context::McpToolOutput;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
@ -18,7 +21,7 @@ impl ToolHandler for McpHandler {
ToolKind::Mcp
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation {
session,
turn,
@ -54,11 +57,19 @@ impl ToolHandler for McpHandler {
.await;
match response {
ResponseInputItem::McpToolCallOutput { result, .. } => Ok(ToolOutput::Mcp { result }),
ResponseInputItem::McpToolCallOutput { result, .. } => {
Ok(Box::new(McpToolOutput { result }))
}
ResponseInputItem::FunctionCallOutput { output, .. } => {
let success = output.success;
let body = output.body;
Ok(ToolOutput::Function { body, success })
match output.body {
codex_protocol::models::FunctionCallOutputBody::Text(text) => {
Ok(Box::new(TextToolOutput { text, success }))
}
codex_protocol::models::FunctionCallOutputBody::ContentItems(content) => {
Ok(Box::new(ContentToolOutput { content, success }))
}
}
}
_ => Err(FunctionCallError::RespondToModel(
"mcp handler received unexpected response variant".to_string(),

View file

@ -1,4 +1,3 @@
use codex_protocol::models::FunctionCallOutputBody;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
@ -25,8 +24,9 @@ use crate::protocol::EventMsg;
use crate::protocol::McpInvocation;
use crate::protocol::McpToolCallBeginEvent;
use crate::protocol::McpToolCallEndEvent;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
@ -184,7 +184,7 @@ impl ToolHandler for McpResourceHandler {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation {
session,
turn,
@ -245,7 +245,7 @@ async fn handle_list_resources(
turn: Arc<TurnContext>,
call_id: String,
arguments: Option<Value>,
) -> Result<ToolOutput, FunctionCallError> {
) -> Result<ToolOutputBox, FunctionCallError> {
let args: ListResourcesArgs = parse_args_with_default(arguments.clone())?;
let ListResourcesArgs { server, cursor } = args;
let server = normalize_optional_string(server);
@ -298,10 +298,12 @@ async fn handle_list_resources(
match payload_result {
Ok(payload) => match serialize_function_output(payload) {
Ok(output) => {
let ToolOutput::Function { body, success } = &output else {
unreachable!("MCP resource handler should return function output");
let Some(output_text) =
(&*output as &dyn std::any::Any).downcast_ref::<TextToolOutput>()
else {
unreachable!("MCP resource handler should return text output");
};
let content = body.to_text().unwrap_or_default();
let content = output_text.text.clone();
let duration = start.elapsed();
emit_tool_call_end(
&session,
@ -309,7 +311,7 @@ async fn handle_list_resources(
&call_id,
invocation,
duration,
Ok(call_tool_result_from_content(&content, *success)),
Ok(call_tool_result_from_content(&content, output_text.success)),
)
.await;
Ok(output)
@ -351,7 +353,7 @@ async fn handle_list_resource_templates(
turn: Arc<TurnContext>,
call_id: String,
arguments: Option<Value>,
) -> Result<ToolOutput, FunctionCallError> {
) -> Result<ToolOutputBox, FunctionCallError> {
let args: ListResourceTemplatesArgs = parse_args_with_default(arguments.clone())?;
let ListResourceTemplatesArgs { server, cursor } = args;
let server = normalize_optional_string(server);
@ -406,10 +408,12 @@ async fn handle_list_resource_templates(
match payload_result {
Ok(payload) => match serialize_function_output(payload) {
Ok(output) => {
let ToolOutput::Function { body, success } = &output else {
unreachable!("MCP resource handler should return function output");
let Some(output_text) =
(&*output as &dyn std::any::Any).downcast_ref::<TextToolOutput>()
else {
unreachable!("MCP resource handler should return text output");
};
let content = body.to_text().unwrap_or_default();
let content = output_text.text.clone();
let duration = start.elapsed();
emit_tool_call_end(
&session,
@ -417,7 +421,7 @@ async fn handle_list_resource_templates(
&call_id,
invocation,
duration,
Ok(call_tool_result_from_content(&content, *success)),
Ok(call_tool_result_from_content(&content, output_text.success)),
)
.await;
Ok(output)
@ -459,7 +463,7 @@ async fn handle_read_resource(
turn: Arc<TurnContext>,
call_id: String,
arguments: Option<Value>,
) -> Result<ToolOutput, FunctionCallError> {
) -> Result<ToolOutputBox, FunctionCallError> {
let args: ReadResourceArgs = parse_args(arguments.clone())?;
let ReadResourceArgs { server, uri } = args;
let server = normalize_required_string("server", server)?;
@ -499,10 +503,12 @@ async fn handle_read_resource(
match payload_result {
Ok(payload) => match serialize_function_output(payload) {
Ok(output) => {
let ToolOutput::Function { body, success } = &output else {
unreachable!("MCP resource handler should return function output");
let Some(output_text) =
(&*output as &dyn std::any::Any).downcast_ref::<TextToolOutput>()
else {
unreachable!("MCP resource handler should return text output");
};
let content = body.to_text().unwrap_or_default();
let content = output_text.text.clone();
let duration = start.elapsed();
emit_tool_call_end(
&session,
@ -510,7 +516,7 @@ async fn handle_read_resource(
&call_id,
invocation,
duration,
Ok(call_tool_result_from_content(&content, *success)),
Ok(call_tool_result_from_content(&content, output_text.success)),
)
.await;
Ok(output)
@ -614,7 +620,7 @@ fn normalize_required_string(field: &str, value: String) -> Result<String, Funct
}
}
fn serialize_function_output<T>(payload: T) -> Result<ToolOutput, FunctionCallError>
fn serialize_function_output<T>(payload: T) -> Result<ToolOutputBox, FunctionCallError>
where
T: Serialize,
{
@ -624,10 +630,10 @@ where
))
})?;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
})
}))
}
fn parse_arguments(raw_args: &str) -> Result<Option<Value>, FunctionCallError> {

View file

@ -13,8 +13,9 @@ use crate::config::Config;
use crate::error::CodexErr;
use crate::features::Feature;
use crate::function_tool::FunctionCallError;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
@ -22,7 +23,6 @@ use crate::tools::registry::ToolKind;
use async_trait::async_trait;
use codex_protocol::ThreadId;
use codex_protocol::models::BaseInstructions;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::protocol::CollabAgentInteractionBeginEvent;
use codex_protocol::protocol::CollabAgentInteractionEndEvent;
use codex_protocol::protocol::CollabAgentRef;
@ -65,7 +65,7 @@ impl ToolHandler for MultiAgentHandler {
matches!(payload, ToolPayload::Function { .. })
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation {
session,
turn,
@ -127,7 +127,7 @@ mod spawn {
turn: Arc<TurnContext>,
call_id: String,
arguments: String,
) -> Result<ToolOutput, FunctionCallError> {
) -> Result<ToolOutputBox, FunctionCallError> {
let args: SpawnAgentArgs = parse_arguments(&arguments)?;
let role_name = args
.agent_type
@ -225,10 +225,10 @@ mod spawn {
FunctionCallError::Fatal(format!("failed to serialize spawn_agent result: {err}"))
})?;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
})
}))
}
}
@ -255,7 +255,7 @@ mod send_input {
turn: Arc<TurnContext>,
call_id: String,
arguments: String,
) -> Result<ToolOutput, FunctionCallError> {
) -> Result<ToolOutputBox, FunctionCallError> {
let args: SendInputArgs = parse_arguments(&arguments)?;
let receiver_thread_id = agent_id(&args.id)?;
let input_items = parse_collab_input(args.message, args.items)?;
@ -318,10 +318,10 @@ mod send_input {
FunctionCallError::Fatal(format!("failed to serialize send_input result: {err}"))
})?;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
})
}))
}
}
@ -345,7 +345,7 @@ mod resume_agent {
turn: Arc<TurnContext>,
call_id: String,
arguments: String,
) -> Result<ToolOutput, FunctionCallError> {
) -> Result<ToolOutputBox, FunctionCallError> {
let args: ResumeAgentArgs = parse_arguments(&arguments)?;
let receiver_thread_id = agent_id(&args.id)?;
let (receiver_agent_nickname, receiver_agent_role) = session
@ -432,10 +432,10 @@ mod resume_agent {
FunctionCallError::Fatal(format!("failed to serialize resume_agent result: {err}"))
})?;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
})
}))
}
async fn try_resume_closed_agent(
@ -495,7 +495,7 @@ pub(crate) mod wait {
turn: Arc<TurnContext>,
call_id: String,
arguments: String,
) -> Result<ToolOutput, FunctionCallError> {
) -> Result<ToolOutputBox, FunctionCallError> {
let args: WaitArgs = parse_arguments(&arguments)?;
if args.ids.is_empty() {
return Err(FunctionCallError::RespondToModel(
@ -645,10 +645,10 @@ pub(crate) mod wait {
FunctionCallError::Fatal(format!("failed to serialize wait result: {err}"))
})?;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: None,
})
}))
}
async fn wait_for_final_status(
@ -688,7 +688,7 @@ pub mod close_agent {
turn: Arc<TurnContext>,
call_id: String,
arguments: String,
) -> Result<ToolOutput, FunctionCallError> {
) -> Result<ToolOutputBox, FunctionCallError> {
let args: CloseAgentArgs = parse_arguments(&arguments)?;
let agent_id = agent_id(&args.id)?;
let (receiver_agent_nickname, receiver_agent_role) = session
@ -765,10 +765,10 @@ pub mod close_agent {
FunctionCallError::Fatal(format!("failed to serialize close_agent result: {err}"))
})?;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
})
}))
}
}
@ -993,6 +993,8 @@ mod tests {
use crate::protocol::SandboxPolicy;
use crate::protocol::SessionSource;
use crate::protocol::SubAgentSource;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::turn_diff_tracker::TurnDiffTracker;
use codex_protocol::ThreadId;
use codex_protocol::models::ContentItem;
@ -1038,6 +1040,13 @@ mod tests {
)
}
fn expect_text_output(output: ToolOutputBox) -> (String, Option<bool>) {
let output = (&*output as &dyn std::any::Any)
.downcast_ref::<TextToolOutput>()
.expect("expected text output");
(output.text.clone(), output.success)
}
#[tokio::test]
async fn handler_rejects_non_function_payloads() {
let (session, turn) = make_session_and_context().await;
@ -1160,13 +1169,7 @@ mod tests {
.handle(invocation)
.await
.expect("spawn_agent should succeed");
let ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
..
} = output
else {
panic!("expected function output");
};
let (content, _) = expect_text_output(output);
let result: SpawnAgentResult =
serde_json::from_str(&content).expect("spawn_agent result should be json");
let agent_id = agent_id(&result.agent_id).expect("agent_id should be valid");
@ -1259,13 +1262,7 @@ mod tests {
.handle(invocation)
.await
.expect("spawn_agent should succeed");
let ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
..
} = output
else {
panic!("expected function output");
};
let (content, _) = expect_text_output(output);
let result: SpawnAgentResult =
serde_json::from_str(&content).expect("spawn_agent result should be json");
let agent_id = agent_id(&result.agent_id).expect("agent_id should be valid");
@ -1349,14 +1346,7 @@ mod tests {
.handle(invocation)
.await
.expect("spawn should succeed within configured depth");
let ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
success,
..
} = output
else {
panic!("expected function output");
};
let (content, success) = expect_text_output(output);
let result: SpawnAgentResult =
serde_json::from_str(&content).expect("spawn_agent result should be json");
assert!(!result.agent_id.is_empty());
@ -1601,14 +1591,7 @@ mod tests {
.handle(invocation)
.await
.expect("resume_agent should succeed");
let ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
success,
..
} = output
else {
panic!("expected function output");
};
let (content, success) = expect_text_output(output);
let result: resume_agent::ResumeAgentResult =
serde_json::from_str(&content).expect("resume_agent result should be json");
assert_eq!(result.status, status_before);
@ -1670,14 +1653,7 @@ mod tests {
.handle(resume_invocation)
.await
.expect("resume_agent should succeed");
let ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
success,
..
} = output
else {
panic!("expected function output");
};
let (content, success) = expect_text_output(output);
let result: resume_agent::ResumeAgentResult =
serde_json::from_str(&content).expect("resume_agent result should be json");
assert_ne!(result.status, AgentStatus::NotFound);
@ -1693,14 +1669,7 @@ mod tests {
.handle(send_invocation)
.await
.expect("send_input should succeed after resume");
let ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
success,
..
} = output
else {
panic!("expected function output");
};
let (content, success) = expect_text_output(output);
let result: serde_json::Value =
serde_json::from_str(&content).expect("send_input result should be json");
let submission_id = result
@ -1825,14 +1794,7 @@ mod tests {
.handle(invocation)
.await
.expect("wait should succeed");
let ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
success,
..
} = output
else {
panic!("expected function output");
};
let (content, success) = expect_text_output(output);
let result: wait::WaitResult =
serde_json::from_str(&content).expect("wait result should be json");
assert_eq!(
@ -1869,14 +1831,7 @@ mod tests {
.handle(invocation)
.await
.expect("wait should succeed");
let ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
success,
..
} = output
else {
panic!("expected function output");
};
let (content, success) = expect_text_output(output);
let result: wait::WaitResult =
serde_json::from_str(&content).expect("wait result should be json");
assert_eq!(
@ -1966,14 +1921,7 @@ mod tests {
.handle(invocation)
.await
.expect("wait should succeed");
let ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
success,
..
} = output
else {
panic!("expected function output");
};
let (content, success) = expect_text_output(output);
let result: wait::WaitResult =
serde_json::from_str(&content).expect("wait result should be json");
assert_eq!(
@ -2006,14 +1954,7 @@ mod tests {
.handle(invocation)
.await
.expect("close_agent should succeed");
let ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
success,
..
} = output
else {
panic!("expected function output");
};
let (content, success) = expect_text_output(output);
let result: close_agent::CloseAgentResult =
serde_json::from_str(&content).expect("close_agent result should be json");
assert_eq!(result.status, status_before);

View file

@ -3,15 +3,15 @@ use crate::client_common::tools::ToolSpec;
use crate::codex::Session;
use crate::codex::TurnContext;
use crate::function_tool::FunctionCallError;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use crate::tools::spec::JsonSchema;
use async_trait::async_trait;
use codex_protocol::config_types::ModeKind;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::plan_tool::UpdatePlanArgs;
use codex_protocol::protocol::EventMsg;
use std::collections::BTreeMap;
@ -67,7 +67,7 @@ impl ToolHandler for PlanHandler {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation {
session,
turn,
@ -88,10 +88,10 @@ impl ToolHandler for PlanHandler {
let content =
handle_update_plan(session.as_ref(), turn.as_ref(), arguments, call_id).await?;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
})
}))
}
}

View file

@ -1,4 +1,3 @@
use codex_protocol::models::FunctionCallOutputBody;
use std::collections::VecDeque;
use std::path::PathBuf;
@ -7,8 +6,9 @@ use codex_utils_string::take_bytes_at_char_boundary;
use serde::Deserialize;
use crate::function_tool::FunctionCallError;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
@ -98,7 +98,7 @@ impl ToolHandler for ReadFileHandler {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation { payload, .. } = invocation;
let arguments = match payload {
@ -146,10 +146,10 @@ impl ToolHandler for ReadFileHandler {
indentation::read_block(&path, offset, limit, indentation).await?
}
};
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(collected.join("\n")),
Ok(Box::new(TextToolOutput {
text: collected.join("\n"),
success: Some(true),
})
}))
}
}

View file

@ -1,11 +1,11 @@
use async_trait::async_trait;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::request_permissions::RequestPermissionsArgs;
use crate::function_tool::FunctionCallError;
use crate::sandboxing::normalize_additional_permissions;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments_with_base_path;
use crate::tools::registry::ToolHandler;
@ -24,7 +24,7 @@ impl ToolHandler for RequestPermissionsHandler {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation {
session,
turn,
@ -67,9 +67,9 @@ impl ToolHandler for RequestPermissionsHandler {
))
})?;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
})
}))
}
}

View file

@ -1,13 +1,12 @@
use async_trait::async_trait;
use codex_protocol::models::FunctionCallOutputBody;
use crate::function_tool::FunctionCallError;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use async_trait::async_trait;
use codex_protocol::config_types::ModeKind;
use codex_protocol::config_types::TUI_VISIBLE_COLLABORATION_MODES;
use codex_protocol::request_user_input::RequestUserInputArgs;
@ -63,7 +62,7 @@ impl ToolHandler for RequestUserInputHandler {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation {
session,
turn,
@ -116,10 +115,10 @@ impl ToolHandler for RequestUserInputHandler {
))
})?;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
})
}))
}
}

View file

@ -3,7 +3,6 @@ use bm25::Document;
use bm25::Language;
use bm25::SearchEngineBuilder;
use codex_app_server_protocol::AppInfo;
use codex_protocol::models::FunctionCallOutputBody;
use serde::Deserialize;
use serde_json::json;
use std::collections::HashMap;
@ -13,8 +12,9 @@ use crate::connectors;
use crate::function_tool::FunctionCallError;
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
use crate::mcp_connection_manager::ToolInfo;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
@ -78,7 +78,7 @@ impl ToolHandler for SearchToolBm25Handler {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation {
payload,
session,
@ -141,10 +141,10 @@ impl ToolHandler for SearchToolBm25Handler {
"tools": [],
})
.to_string();
return Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
return Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
});
}));
}
let documents: Vec<Document<usize>> = entries
@ -184,10 +184,10 @@ impl ToolHandler for SearchToolBm25Handler {
})
.to_string();
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
})
}))
}
}

View file

@ -1,6 +1,5 @@
use async_trait::async_trait;
use codex_protocol::ThreadId;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::ShellCommandToolCallParams;
use codex_protocol::models::ShellToolCallParams;
use std::sync::Arc;
@ -15,8 +14,9 @@ use crate::is_safe_command::is_known_safe_command;
use crate::protocol::ExecCommandSource;
use crate::shell::Shell;
use crate::skills::maybe_emit_implicit_skill_invocation;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::events::ToolEmitter;
use crate::tools::events::ToolEventCtx;
@ -165,7 +165,7 @@ impl ToolHandler for ShellHandler {
}
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation {
session,
turn,
@ -253,7 +253,7 @@ impl ToolHandler for ShellCommandHandler {
.unwrap_or(true)
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation {
session,
turn,
@ -305,7 +305,7 @@ impl ToolHandler for ShellCommandHandler {
}
impl ShellHandler {
async fn run_exec_like(args: RunExecLikeArgs) -> Result<ToolOutput, FunctionCallError> {
async fn run_exec_like(args: RunExecLikeArgs) -> Result<ToolOutputBox, FunctionCallError> {
let RunExecLikeArgs {
tool_name,
exec_params,
@ -449,10 +449,10 @@ impl ShellHandler {
.map(|result| result.output);
let event_ctx = ToolEventCtx::new(session.as_ref(), turn.as_ref(), &call_id, None);
let content = emitter.finish(event_ctx, out).await?;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
})
}))
}
}

View file

@ -1,4 +1,3 @@
use codex_protocol::models::FunctionCallOutputBody;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::sync::Arc;
@ -11,8 +10,9 @@ use tokio::sync::Barrier;
use tokio::time::sleep;
use crate::function_tool::FunctionCallError;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
@ -61,7 +61,7 @@ impl ToolHandler for TestSyncHandler {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation { payload, .. } = invocation;
let arguments = match payload {
@ -91,10 +91,10 @@ impl ToolHandler for TestSyncHandler {
sleep(Duration::from_millis(delay)).await;
}
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text("ok".to_string()),
Ok(Box::new(TextToolOutput {
text: "ok".to_string(),
success: Some(true),
})
}))
}
}

View file

@ -7,8 +7,9 @@ use crate::sandboxing::SandboxPermissions;
use crate::shell::Shell;
use crate::shell::get_shell_by_model_provided_path;
use crate::skills::maybe_emit_implicit_skill_invocation;
use crate::tools::context::TextToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::apply_granted_turn_permissions;
use crate::tools::handlers::apply_patch::intercept_apply_patch;
@ -24,7 +25,6 @@ use crate::unified_exec::UnifiedExecProcessManager;
use crate::unified_exec::UnifiedExecResponse;
use crate::unified_exec::WriteStdinRequest;
use async_trait::async_trait;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::PermissionProfile;
use serde::Deserialize;
use std::path::PathBuf;
@ -114,7 +114,7 @@ impl ToolHandler for UnifiedExecHandler {
!is_known_safe_command(&command)
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
let ToolInvocation {
session,
turn,
@ -291,10 +291,10 @@ impl ToolHandler for UnifiedExecHandler {
let content = format_response(&response);
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::Text(content),
Ok(Box::new(TextToolOutput {
text: content,
success: Some(true),
})
}))
}
}

View file

@ -1,6 +1,5 @@
use async_trait::async_trait;
use codex_protocol::models::ContentItem;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::models::ImageDetail;
use codex_protocol::models::local_image_content_items_with_label_number;
@ -13,8 +12,9 @@ use crate::features::Feature;
use crate::function_tool::FunctionCallError;
use crate::protocol::EventMsg;
use crate::protocol::ViewImageToolCallEvent;
use crate::tools::context::ContentToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
@ -36,7 +36,7 @@ impl ToolHandler for ViewImageHandler {
ToolKind::Function
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError> {
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError> {
if !invocation
.turn
.model_info
@ -121,9 +121,9 @@ impl ToolHandler for ViewImageHandler {
)
.await;
Ok(ToolOutput::Function {
body: FunctionCallOutputBody::ContentItems(content),
Ok(Box::new(ContentToolOutput {
content,
success: Some(true),
})
}))
}
}

View file

@ -10,7 +10,7 @@ use crate::memories::usage::emit_metric_for_tool_read;
use crate::protocol::SandboxPolicy;
use crate::sandbox_tags::sandbox_tag;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolOutputBox;
use crate::tools::context::ToolPayload;
use async_trait::async_trait;
use codex_hooks::HookEvent;
@ -52,7 +52,7 @@ pub trait ToolHandler: Send + Sync {
/// Perform the actual [ToolInvocation] and returns a [ToolOutput] containing
/// the final output to return to the model.
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, FunctionCallError>;
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutputBox, FunctionCallError>;
}
pub struct ToolRegistry {