From 418e5fe615cc8d54acdfbf917ddd7e02ff1f1c81 Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Mon, 27 Apr 2026 15:03:33 +0200 Subject: [PATCH] Implement tool result conversion for anyhow errors (#55001) Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A --- crates/agent/src/thread.rs | 29 ++++++++++--------- .../src/tools/context_server_registry.rs | 21 ++++++++------ crates/language_model_core/src/request.rs | 6 ++++ 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 2a8a6a5b3c..07e6facb1f 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -2448,9 +2448,10 @@ impl Thread { .collect(); (false, output) } else { - let output = AgentToolOutput::from_error( + let output = anyhow::anyhow!( "Attempted to read an image, but this model doesn't support it.", - ); + ) + .into(); (true, output) } } else { @@ -3426,14 +3427,15 @@ pub struct AgentToolOutput { pub raw_output: serde_json::Value, } -impl AgentToolOutput { - pub fn from_error(message: impl Into) -> Self { - let message = message.into(); - let llm_output = vec![LanguageModelToolResultContent::Text(Arc::from( - message.as_str(), - ))]; +impl From for AgentToolOutput { + fn from(error: anyhow::Error) -> Self { + let llm_output = vec![error.into()]; + let raw_output = serde_json::to_value(&llm_output).unwrap_or_else(|e| { + log::error!("Failed to serialize tool output: {e}"); + serde_json::Value::Null + }); Self { - raw_output: serde_json::Value::String(message), + raw_output, llm_output, } } @@ -3512,12 +3514,13 @@ where let task = self.0.clone().run(tool_input, event_stream, cx); cx.spawn(async move |_cx| match task.await { Ok(output) => { - let raw_output = serde_json::to_value(&output).map_err(|e| { - AgentToolOutput::from_error(format!("Failed to serialize tool output: {e}")) - })?; + let raw_output = serde_json::to_value(&output).unwrap_or_else(|e| { + log::error!("Failed to serialize tool output: {e}"); + serde_json::Value::Null + }); Ok(AgentToolOutput { - llm_output: vec![output.into()], raw_output, + llm_output: vec![output.into()], }) } Err(error_output) => { diff --git a/crates/agent/src/tools/context_server_registry.rs b/crates/agent/src/tools/context_server_registry.rs index c5476d6343..261c89e6b8 100644 --- a/crates/agent/src/tools/context_server_registry.rs +++ b/crates/agent/src/tools/context_server_registry.rs @@ -337,7 +337,7 @@ impl AnyAgentTool for ContextServerTool { cx: &mut App, ) -> Task> { let Some(server) = self.store.read(cx).get_running_server(&self.server_id) else { - return Task::ready(Err(AgentToolOutput::from_error("Context server not found"))); + return Task::ready(Err(anyhow::anyhow!("Context server not found").into())); }; let tool_name = self.tool.name.clone(); let tool_id = mcp_tool_id(&self.server_id.0, &self.tool.name); @@ -347,14 +347,17 @@ impl AnyAgentTool for ContextServerTool { event_stream.authorize_third_party_tool(initial_title, tool_id, display_name, cx); cx.spawn(async move |_cx| { - let input = input.recv().await.map_err(|e| { - AgentToolOutput::from_error(format!("Failed to receive tool input: {e}")) - })?; + let input = input + .recv() + .await + .map_err(|e| anyhow::anyhow!(format!("Failed to receive tool input: {e}")))?; - authorize.await.map_err(|e| AgentToolOutput::from_error(e.to_string()))?; + authorize + .await + .map_err(|e| anyhow::anyhow!(e.to_string()))?; let Some(protocol) = server.client() else { - return Err(AgentToolOutput::from_error("Context server not initialized")); + return Err(anyhow::anyhow!("Context server not initialized").into()); }; let arguments = if let serde_json::Value::Object(map) = input { @@ -378,16 +381,16 @@ impl AnyAgentTool for ContextServerTool { ); let response = futures::select! { - response = request.fuse() => response.map_err(|e| AgentToolOutput::from_error(e.to_string()))?, + response = request.fuse() => response?, _ = event_stream.cancelled_by_user().fuse() => { - return Err(AgentToolOutput::from_error("MCP tool cancelled by user")); + return Err(anyhow::anyhow!("MCP tool cancelled by user").into()); } }; if response.is_error == Some(true) { let error_message: String = response.content.iter().filter_map(|c| c.text()).collect(); - return Err(AgentToolOutput::from_error(error_message)); + return Err(anyhow::anyhow!(error_message).into()); } let mut llm_output = Vec::new(); diff --git a/crates/language_model_core/src/request.rs b/crates/language_model_core/src/request.rs index f352ce16d2..b2b42c091b 100644 --- a/crates/language_model_core/src/request.rs +++ b/crates/language_model_core/src/request.rs @@ -273,6 +273,12 @@ impl From for LanguageModelToolResultContent { } } +impl From for LanguageModelToolResultContent { + fn from(error: anyhow::Error) -> Self { + Self::Text(Arc::from(error.to_string())) + } +} + impl From for LanguageModelToolResultContent { fn from(image: LanguageModelImage) -> Self { Self::Image(image)