agent: Better error messages when tool fails (#55616)

Does not actually seem useful to the LLM to include `Failed to receive
tool input: ...` in the error message. We now only include the actual
error.

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)
- [ ] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- N/A
This commit is contained in:
Bennet Bo Fenner authored and GitHub committed 2026-05-04 10:27:33 +00:00
1 parent 8a8293b3bd
commit 193a8c75a3
21 files changed
+30 -93

No files matched your search

+2 -8
View File
@@ -4063,14 +4063,8 @@ async fn test_streaming_tool_completes_when_llm_stream_ends_without_final_input(
tool_use_id: tool_use.id.clone(),
tool_name: tool_use.name,
is_error: true,
content: vec![
"Failed to receive tool input: tool input was not fully received"
.into(),
],
output: Some(
"Failed to receive tool input: tool input was not fully received"
.into()
),
content: vec!["tool input was not fully received".into(),],
output: Some("tool input was not fully received".into()),
}
)],
cache: true,
+9 -33
View File
@@ -61,10 +61,7 @@ impl AgentTool for StreamingEchoTool {
) -> Task<Result<String, String>> {
let wait_until_complete_rx = self.wait_until_complete_rx.lock().unwrap().take();
cx.spawn(async move |_cx| {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let input = input.recv().await.map_err(|e| e.to_string())?;
if let Some(rx) = wait_until_complete_rx {
rx.await.ok();
}
@@ -127,7 +124,7 @@ impl AgentTool for StreamingJsonErrorContextTool {
));
}
Err(error) => {
return Err(format!("Failed to receive tool input: {error}"));
return Err(error.to_string());
}
}
}
@@ -220,10 +217,7 @@ impl AgentTool for EchoTool {
cx: &mut App,
) -> Task<Result<String, String>> {
cx.spawn(async move |_cx| {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let input = input.recv().await.map_err(|e| e.to_string())?;
Ok(input.text)
})
}
@@ -271,10 +265,7 @@ impl AgentTool for DelayTool {
{
let executor = cx.background_executor().clone();
cx.foreground_executor().spawn(async move {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let input = input.recv().await.map_err(|e| e.to_string())?;
executor.timer(Duration::from_millis(input.ms)).await;
Ok("Ding".to_string())
})
@@ -311,10 +302,7 @@ impl AgentTool for ToolRequiringPermission {
cx: &mut App,
) -> Task<Result<String, String>> {
cx.spawn(async move |cx| {
let _input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let _input = input.recv().await.map_err(|e| e.to_string())?;
let authorize = cx.update(|cx| {
let context = crate::ToolPermissionContext::new(Self::NAME, vec![String::new()]);
@@ -359,10 +347,7 @@ impl AgentTool for ToolRequiringPermission2 {
cx: &mut App,
) -> Task<Result<String, String>> {
cx.spawn(async move |cx| {
let _input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let _input = input.recv().await.map_err(|e| e.to_string())?;
let authorize = cx.update(|cx| {
let context = crate::ToolPermissionContext::new(Self::NAME, vec![String::new()]);
@@ -404,10 +389,7 @@ impl AgentTool for InfiniteTool {
cx: &mut App,
) -> Task<Result<String, String>> {
cx.foreground_executor().spawn(async move {
let _input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let _input = input.recv().await.map_err(|e| e.to_string())?;
future::pending::<()>().await;
unreachable!()
})
@@ -460,10 +442,7 @@ impl AgentTool for CancellationAwareTool {
cx: &mut App,
) -> Task<Result<String, String>> {
cx.foreground_executor().spawn(async move {
let _input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let _input = input.recv().await.map_err(|e| e.to_string())?;
// Wait for cancellation - this tool does nothing but wait to be cancelled
event_stream.cancelled_by_user().await;
self.was_cancelled.store(true, Ordering::SeqCst);
@@ -519,10 +498,7 @@ impl AgentTool for WordListTool {
cx: &mut App,
) -> Task<Result<String, String>> {
cx.spawn(async move |_cx| {
let _input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let _input = input.recv().await.map_err(|e| e.to_string())?;
Ok("ok".to_string())
})
}
@@ -350,7 +350,7 @@ impl AnyAgentTool for ContextServerTool {
let input = input
.recv()
.await
.map_err(|e| anyhow::anyhow!(format!("Failed to receive tool input: {e}")))?;
.map_err(|e| anyhow::anyhow!(e.to_string()))?;
authorize
.await
+1 -4
View File
@@ -88,10 +88,7 @@ impl AgentTool for CopyPathTool {
) -> Task<Result<Self::Output, Self::Output>> {
let project = self.project.clone();
cx.spawn(async move |cx| {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let input = input.recv().await.map_err(|e| e.to_string())?;
let paths = vec![input.source_path.clone(), input.destination_path.clone()];
let decision = cx.update(|cx| {
decide_permission_for_paths(Self::NAME, &paths, &AgentSettings::get_global(cx))
@@ -77,10 +77,7 @@ impl AgentTool for CreateDirectoryTool {
) -> Task<Result<Self::Output, Self::Output>> {
let project = self.project.clone();
cx.spawn(async move |cx| {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let input = input.recv().await.map_err(|e| e.to_string())?;
let decision = cx.update(|cx| {
decide_permission_for_path(Self::NAME, &input.path, AgentSettings::get_global(cx))
});
+1 -4
View File
@@ -81,10 +81,7 @@ impl AgentTool for DeletePathTool {
let project = self.project.clone();
let action_log = self.action_log.clone();
cx.spawn(async move |cx| {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let input = input.recv().await.map_err(|e| e.to_string())?;
let path = input.path;
let decision = cx.update(|cx| {
+1 -4
View File
@@ -93,10 +93,7 @@ impl AgentTool for DiagnosticsTool {
) -> Task<Result<Self::Output, Self::Output>> {
let project = self.project.clone();
cx.spawn(async move |cx| {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let input = input.recv().await.map_err(|e| e.to_string())?;
match input.path {
Some(path) if !path.is_empty() => {
+1 -1
View File
@@ -439,7 +439,7 @@ impl EditFileTool {
},
Err(error) => {
return EditSessionResult::Failed {
error: format!("Failed to receive tool input: {error}"),
error: error.to_string(),
session,
};
}
+1 -4
View File
@@ -143,10 +143,7 @@ impl AgentTool for FetchTool {
) -> Task<Result<Self::Output, Self::Output>> {
let http_client = self.http_client.clone();
cx.spawn(async move |cx| {
let input: FetchToolInput = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let input: FetchToolInput = input.recv().await.map_err(|e| e.to_string())?;
let authorize = cx.update(|cx| {
let context =
+1 -1
View File
@@ -128,7 +128,7 @@ impl AgentTool for FindPathTool {
let project = self.project.clone();
cx.spawn(async move |cx| {
let input = input.recv().await.map_err(|e| FindPathToolOutput::Error {
error: format!("Failed to receive tool input: {e}"),
error: e.to_string(),
})?;
let search_paths_task = cx.update(|cx| search_paths(&input.glob, project, cx));
+1 -1
View File
@@ -126,7 +126,7 @@ impl AgentTool for GrepTool {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
.map_err(|e| e.to_string())?;
let results = cx.update(|cx| {
let path_style = project.read(cx).path_style(cx);
@@ -155,7 +155,7 @@ impl AgentTool for ListDirectoryTool {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
.map_err(|e| e.to_string())?;
// Sometimes models will return these even though we tell it to give a path and not a glob.
// When this happens, just list the root worktree directories.
+1 -1
View File
@@ -104,7 +104,7 @@ impl AgentTool for MovePathTool {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
.map_err(|e| e.to_string())?;
let paths = vec![input.source_path.clone(), input.destination_path.clone()];
let decision = cx.update(|cx| {
decide_permission_for_paths(Self::NAME, &paths, AgentSettings::get_global(cx))
+1 -4
View File
@@ -55,10 +55,7 @@ impl AgentTool for NowTool {
cx: &mut App,
) -> Task<Result<String, String>> {
cx.spawn(async move |_cx| {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let input = input.recv().await.map_err(|e| e.to_string())?;
let now = match input.timezone {
Timezone::Utc => Utc::now().to_rfc3339(),
Timezone::Local => Local::now().to_rfc3339(),
+1 -4
View File
@@ -67,10 +67,7 @@ impl AgentTool for OpenTool {
) -> Task<Result<Self::Output, Self::Output>> {
let project = self.project.clone();
cx.spawn(async move |cx| {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let input = input.recv().await.map_err(|e| e.to_string())?;
// If path_or_url turns out to be a path in the project, make it absolute.
let (abs_path, initial_title) = cx.update(|cx| {
@@ -75,10 +75,7 @@ impl AgentTool for RestoreFileFromDiskTool {
let project = self.project.clone();
cx.spawn(async move |cx| {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let input = input.recv().await.map_err(|e| e.to_string())?;
// Check for any immediate deny before doing async work.
for path in &input.paths {
+1 -4
View File
@@ -72,10 +72,7 @@ impl AgentTool for SaveFileTool {
let project = self.project.clone();
cx.spawn(async move |cx| {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let input = input.recv().await.map_err(|e| e.to_string())?;
// Check for any immediate deny before doing async work.
for path in &input.paths {
+1 -1
View File
@@ -137,7 +137,7 @@ impl AgentTool for SpawnAgentTool {
.await
.map_err(|e| SpawnAgentToolOutput::Error {
session_id: None,
error: format!("Failed to receive tool input: {e}"),
error: e.to_string(),
session_info: None,
})?;
+1 -4
View File
@@ -89,10 +89,7 @@ impl AgentTool for TerminalTool {
cx: &mut App,
) -> Task<Result<Self::Output, Self::Output>> {
cx.spawn(async move |cx| {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let input = input.recv().await.map_err(|e| e.to_string())?;
let (working_dir, authorize) = cx.update(|cx| {
let working_dir =
+1 -4
View File
@@ -90,10 +90,7 @@ impl AgentTool for UpdatePlanTool {
cx: &mut App,
) -> Task<Result<Self::Output, Self::Output>> {
cx.spawn(async move |_cx| {
let input = input
.recv()
.await
.map_err(|e| format!("Failed to receive tool input: {e}"))?;
let input = input.recv().await.map_err(|e| e.to_string())?;
event_stream.update_plan(Self::to_plan(input));
+1 -1
View File
@@ -78,7 +78,7 @@ impl AgentTool for WebSearchTool {
.recv()
.await
.map_err(|e| WebSearchToolOutput::Error {
error: format!("Failed to receive tool input: {e}"),
error: e.to_string(),
})?;
let authorize = cx.update(|cx| {