From ff73838baba9d6f10c6ed803436fe8892e4c09f5 Mon Sep 17 00:00:00 2001 From: Katie Geer Date: Thu, 22 Jan 2026 09:43:48 -0800 Subject: [PATCH] Add "Agent Panel Error Shown" telemetry with ACP error details (#46848) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new "Agent Panel Error Shown" telemetry event that fires when users see errors in the agent panel. For errors from external ACP agents (like Claude Code), we capture additional details. Previously, we had no visibility into what errors users were encountering in the agent panel. This made it difficult to diagnose issues, especially with external agents. The new telemetry event includes: - `agent` — The agent telemetry ID - `session_id` — The session ID - `kind` — Error category (payment_required, authentication_required, refusal, other, etc.) - `acp_error_code` — The ACP error code when available (e.g., "InternalError") - `acp_error_message` — The ACP error message when available Release Notes: - N/A --------- Co-authored-by: Michael Benfield --- crates/agent_ui/src/acp/thread_view.rs | 85 +++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 7c204e9b0c4122306c20161446fccb6ad0e7a66d..3f03ecf32257fb2c5650de0289e37ac3a6efe139 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -99,7 +99,10 @@ enum ThreadError { PaymentRequired, Refusal, AuthenticationRequired(SharedString), - Other(SharedString), + Other { + message: SharedString, + acp_error_code: Option, + }, } impl ThreadError { @@ -111,16 +114,25 @@ impl ThreadError { { Self::AuthenticationRequired(acp_error.message.clone().into()) } else { - let string = format!("{:#}", error); + let message: SharedString = format!("{:#}", error).into(); + + // Extract ACP error code if available + let acp_error_code = error + .downcast_ref::() + .map(|acp_error| SharedString::from(acp_error.code.to_string())); + // TODO: we should have Gemini return better errors here. if agent.clone().downcast::().is_some() - && string.contains("Could not load the default credentials") - || string.contains("API key not valid") - || string.contains("Request had invalid authentication credentials") + && message.contains("Could not load the default credentials") + || message.contains("API key not valid") + || message.contains("Request had invalid authentication credentials") { - Self::AuthenticationRequired(string.into()) + Self::AuthenticationRequired(message) } else { - Self::Other(string.into()) + Self::Other { + message, + acp_error_code, + } } } } @@ -1932,10 +1944,59 @@ impl AcpThreadView { } fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context) { - self.thread_error = Some(ThreadError::from_err(error, &self.agent)); + let thread_error = ThreadError::from_err(error, &self.agent); + self.emit_thread_error_telemetry(&thread_error, cx); + self.thread_error = Some(thread_error); cx.notify(); } + fn emit_thread_error_telemetry(&self, error: &ThreadError, cx: &mut Context) { + let (error_kind, acp_error_code, message): (&str, Option, SharedString) = + match error { + ThreadError::PaymentRequired => ( + "payment_required", + None, + "You reached your free usage limit. Upgrade to Zed Pro for more prompts." + .into(), + ), + ThreadError::Refusal => { + let model_or_agent_name = self.current_model_name(cx); + let message = format!( + "{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.", + model_or_agent_name + ); + ("refusal", None, message.into()) + } + ThreadError::AuthenticationRequired(message) => { + ("authentication_required", None, message.clone()) + } + ThreadError::Other { + acp_error_code, + message, + } => ("other", acp_error_code.clone(), message.clone()), + }; + + let (agent_telemetry_id, session_id) = self + .thread() + .map(|t| { + let thread = t.read(cx); + ( + thread.connection().telemetry_id(), + thread.session_id().clone(), + ) + }) + .unzip(); + + telemetry::event!( + "Agent Panel Error Shown", + agent = agent_telemetry_id, + session_id = session_id, + kind = error_kind, + acp_error_code = acp_error_code, + message = message, + ); + } + fn clear_thread_error(&mut self, cx: &mut Context) { self.thread_error = None; self.thread_error_markdown = None; @@ -2018,7 +2079,9 @@ impl AcpThreadView { } AcpThreadEvent::Refusal => { self.thread_retry_status.take(); - self.thread_error = Some(ThreadError::Refusal); + let thread_error = ThreadError::Refusal; + self.emit_thread_error_telemetry(&thread_error, cx); + self.thread_error = Some(thread_error); let model_or_agent_name = self.current_model_name(cx); let notification_message = format!("{} refused to respond to this request", model_or_agent_name); @@ -7844,7 +7907,9 @@ impl AcpThreadView { fn render_thread_error(&mut self, window: &mut Window, cx: &mut Context) -> Option
{ let content = match self.thread_error.as_ref()? { - ThreadError::Other(error) => self.render_any_thread_error(error.clone(), window, cx), + ThreadError::Other { message, .. } => { + self.render_any_thread_error(message.clone(), window, cx) + } ThreadError::Refusal => self.render_refusal_error(cx), ThreadError::AuthenticationRequired(error) => { self.render_authentication_required_error(error.clone(), cx)