acp: Remove ACP v0 (#36785)

Conrad Irwin created

We had a few people confused about why some features weren't working due
to the fallback logic.

It's gone.

Release Notes:

- N/A

Change summary

Cargo.lock                        |  61 ----
Cargo.toml                        |   1 
crates/agent_servers/Cargo.toml   |   1 
crates/agent_servers/src/acp.rs   | 389 +++++++++++++++++++++++++++++++-
tooling/workspace-hack/Cargo.toml |   2 
5 files changed, 382 insertions(+), 72 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -289,7 +289,6 @@ dependencies = [
  "action_log",
  "agent-client-protocol",
  "agent_settings",
- "agentic-coding-protocol",
  "anyhow",
  "client",
  "collections",
@@ -443,24 +442,6 @@ dependencies = [
  "zed_actions",
 ]
 
-[[package]]
-name = "agentic-coding-protocol"
-version = "0.0.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4"
-dependencies = [
- "anyhow",
- "chrono",
- "derive_more 2.0.1",
- "futures 0.3.31",
- "log",
- "parking_lot",
- "schemars",
- "semver",
- "serde",
- "serde_json",
-]
-
 [[package]]
 name = "ahash"
 version = "0.7.8"
@@ -876,7 +857,7 @@ dependencies = [
  "anyhow",
  "async-trait",
  "collections",
- "derive_more 0.99.19",
+ "derive_more",
  "extension",
  "futures 0.3.31",
  "gpui",
@@ -939,7 +920,7 @@ dependencies = [
  "clock",
  "collections",
  "ctor",
- "derive_more 0.99.19",
+ "derive_more",
  "gpui",
  "icons",
  "indoc",
@@ -976,7 +957,7 @@ dependencies = [
  "cloud_llm_client",
  "collections",
  "component",
- "derive_more 0.99.19",
+ "derive_more",
  "diffy",
  "editor",
  "feature_flags",
@@ -3089,7 +3070,7 @@ dependencies = [
  "cocoa 0.26.0",
  "collections",
  "credentials_provider",
- "derive_more 0.99.19",
+ "derive_more",
  "feature_flags",
  "fs",
  "futures 0.3.31",
@@ -3521,7 +3502,7 @@ name = "command_palette_hooks"
 version = "0.1.0"
 dependencies = [
  "collections",
- "derive_more 0.99.19",
+ "derive_more",
  "gpui",
  "workspace-hack",
 ]
@@ -4684,27 +4665,6 @@ dependencies = [
  "syn 2.0.101",
 ]
 
-[[package]]
-name = "derive_more"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
-dependencies = [
- "derive_more-impl",
-]
-
-[[package]]
-name = "derive_more-impl"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.101",
- "unicode-xid",
-]
-
 [[package]]
 name = "derive_refineable"
 version = "0.1.0"
@@ -6441,7 +6401,7 @@ dependencies = [
  "askpass",
  "async-trait",
  "collections",
- "derive_more 0.99.19",
+ "derive_more",
  "futures 0.3.31",
  "git2",
  "gpui",
@@ -7471,7 +7431,7 @@ dependencies = [
  "core-video",
  "cosmic-text",
  "ctor",
- "derive_more 0.99.19",
+ "derive_more",
  "embed-resource",
  "env_logger 0.11.8",
  "etagere",
@@ -7996,7 +7956,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "bytes 1.10.1",
- "derive_more 0.99.19",
+ "derive_more",
  "futures 0.3.31",
  "http 1.3.1",
  "http-body 1.0.1",
@@ -14399,12 +14359,10 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
 dependencies = [
- "chrono",
  "dyn-clone",
  "indexmap",
  "ref-cast",
  "schemars_derive",
- "semver",
  "serde",
  "serde_json",
 ]
@@ -16488,7 +16446,7 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "collections",
- "derive_more 0.99.19",
+ "derive_more",
  "fs",
  "futures 0.3.31",
  "gpui",
@@ -20003,7 +19961,6 @@ dependencies = [
  "rustix 1.0.7",
  "rustls 0.23.26",
  "rustls-webpki 0.103.1",
- "schemars",
  "scopeguard",
  "sea-orm",
  "sea-query-binder",

Cargo.toml 🔗

@@ -426,7 +426,6 @@ zlog_settings = { path = "crates/zlog_settings" }
 # External crates
 #
 
-agentic-coding-protocol = "0.0.10"
 agent-client-protocol = "0.0.31"
 aho-corasick = "1.1"
 alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }

crates/agent_servers/Cargo.toml 🔗

@@ -22,7 +22,6 @@ acp_thread.workspace = true
 action_log.workspace = true
 agent-client-protocol.workspace = true
 agent_settings.workspace = true
-agentic-coding-protocol.workspace = true
 anyhow.workspace = true
 client = { workspace = true, optional = true }
 collections.workspace = true

crates/agent_servers/src/acp.rs 🔗

@@ -1,34 +1,391 @@
-use std::{path::Path, rc::Rc};
-
 use crate::AgentServerCommand;
 use acp_thread::AgentConnection;
-use anyhow::Result;
-use gpui::AsyncApp;
+use acp_tools::AcpConnectionRegistry;
+use action_log::ActionLog;
+use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
+use anyhow::anyhow;
+use collections::HashMap;
+use futures::AsyncBufReadExt as _;
+use futures::channel::oneshot;
+use futures::io::BufReader;
+use project::Project;
+use serde::Deserialize;
+use std::{any::Any, cell::RefCell};
+use std::{path::Path, rc::Rc};
 use thiserror::Error;
 
-mod v0;
-mod v1;
+use anyhow::{Context as _, Result};
+use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
+
+use acp_thread::{AcpThread, AuthRequired, LoadError};
 
 #[derive(Debug, Error)]
 #[error("Unsupported version")]
 pub struct UnsupportedVersion;
 
+pub struct AcpConnection {
+    server_name: &'static str,
+    connection: Rc<acp::ClientSideConnection>,
+    sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
+    auth_methods: Vec<acp::AuthMethod>,
+    prompt_capabilities: acp::PromptCapabilities,
+    _io_task: Task<Result<()>>,
+}
+
+pub struct AcpSession {
+    thread: WeakEntity<AcpThread>,
+    suppress_abort_err: bool,
+}
+
 pub async fn connect(
     server_name: &'static str,
     command: AgentServerCommand,
     root_dir: &Path,
     cx: &mut AsyncApp,
 ) -> Result<Rc<dyn AgentConnection>> {
-    let conn = v1::AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await;
-
-    match conn {
-        Ok(conn) => Ok(Rc::new(conn) as _),
-        Err(err) if err.is::<UnsupportedVersion>() => {
-            // Consider re-using initialize response and subprocess when adding another version here
-            let conn: Rc<dyn AgentConnection> =
-                Rc::new(v0::AcpConnection::stdio(server_name, command, root_dir, cx).await?);
-            Ok(conn)
+    let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?;
+    Ok(Rc::new(conn) as _)
+}
+
+const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
+
+impl AcpConnection {
+    pub async fn stdio(
+        server_name: &'static str,
+        command: AgentServerCommand,
+        root_dir: &Path,
+        cx: &mut AsyncApp,
+    ) -> Result<Self> {
+        let mut child = util::command::new_smol_command(&command.path)
+            .args(command.args.iter().map(|arg| arg.as_str()))
+            .envs(command.env.iter().flatten())
+            .current_dir(root_dir)
+            .stdin(std::process::Stdio::piped())
+            .stdout(std::process::Stdio::piped())
+            .stderr(std::process::Stdio::piped())
+            .kill_on_drop(true)
+            .spawn()?;
+
+        let stdout = child.stdout.take().context("Failed to take stdout")?;
+        let stdin = child.stdin.take().context("Failed to take stdin")?;
+        let stderr = child.stderr.take().context("Failed to take stderr")?;
+        log::trace!("Spawned (pid: {})", child.id());
+
+        let sessions = Rc::new(RefCell::new(HashMap::default()));
+
+        let client = ClientDelegate {
+            sessions: sessions.clone(),
+            cx: cx.clone(),
+        };
+        let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, {
+            let foreground_executor = cx.foreground_executor().clone();
+            move |fut| {
+                foreground_executor.spawn(fut).detach();
+            }
+        });
+
+        let io_task = cx.background_spawn(io_task);
+
+        cx.background_spawn(async move {
+            let mut stderr = BufReader::new(stderr);
+            let mut line = String::new();
+            while let Ok(n) = stderr.read_line(&mut line).await
+                && n > 0
+            {
+                log::warn!("agent stderr: {}", &line);
+                line.clear();
+            }
+        })
+        .detach();
+
+        cx.spawn({
+            let sessions = sessions.clone();
+            async move |cx| {
+                let status = child.status().await?;
+
+                for session in sessions.borrow().values() {
+                    session
+                        .thread
+                        .update(cx, |thread, cx| {
+                            thread.emit_load_error(LoadError::Exited { status }, cx)
+                        })
+                        .ok();
+                }
+
+                anyhow::Ok(())
+            }
+        })
+        .detach();
+
+        let connection = Rc::new(connection);
+
+        cx.update(|cx| {
+            AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
+                registry.set_active_connection(server_name, &connection, cx)
+            });
+        })?;
+
+        let response = connection
+            .initialize(acp::InitializeRequest {
+                protocol_version: acp::VERSION,
+                client_capabilities: acp::ClientCapabilities {
+                    fs: acp::FileSystemCapability {
+                        read_text_file: true,
+                        write_text_file: true,
+                    },
+                },
+            })
+            .await?;
+
+        if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
+            return Err(UnsupportedVersion.into());
+        }
+
+        Ok(Self {
+            auth_methods: response.auth_methods,
+            connection,
+            server_name,
+            sessions,
+            prompt_capabilities: response.agent_capabilities.prompt_capabilities,
+            _io_task: io_task,
+        })
+    }
+}
+
+impl AgentConnection for AcpConnection {
+    fn new_thread(
+        self: Rc<Self>,
+        project: Entity<Project>,
+        cwd: &Path,
+        cx: &mut App,
+    ) -> Task<Result<Entity<AcpThread>>> {
+        let conn = self.connection.clone();
+        let sessions = self.sessions.clone();
+        let cwd = cwd.to_path_buf();
+        cx.spawn(async move |cx| {
+            let response = conn
+                .new_session(acp::NewSessionRequest {
+                    mcp_servers: vec![],
+                    cwd,
+                })
+                .await
+                .map_err(|err| {
+                    if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
+                        let mut error = AuthRequired::new();
+
+                        if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
+                            error = error.with_description(err.message);
+                        }
+
+                        anyhow!(error)
+                    } else {
+                        anyhow!(err)
+                    }
+                })?;
+
+            let session_id = response.session_id;
+            let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
+            let thread = cx.new(|_cx| {
+                AcpThread::new(
+                    self.server_name,
+                    self.clone(),
+                    project,
+                    action_log,
+                    session_id.clone(),
+                )
+            })?;
+
+            let session = AcpSession {
+                thread: thread.downgrade(),
+                suppress_abort_err: false,
+            };
+            sessions.borrow_mut().insert(session_id, session);
+
+            Ok(thread)
+        })
+    }
+
+    fn auth_methods(&self) -> &[acp::AuthMethod] {
+        &self.auth_methods
+    }
+
+    fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
+        let conn = self.connection.clone();
+        cx.foreground_executor().spawn(async move {
+            let result = conn
+                .authenticate(acp::AuthenticateRequest {
+                    method_id: method_id.clone(),
+                })
+                .await?;
+
+            Ok(result)
+        })
+    }
+
+    fn prompt(
+        &self,
+        _id: Option<acp_thread::UserMessageId>,
+        params: acp::PromptRequest,
+        cx: &mut App,
+    ) -> Task<Result<acp::PromptResponse>> {
+        let conn = self.connection.clone();
+        let sessions = self.sessions.clone();
+        let session_id = params.session_id.clone();
+        cx.foreground_executor().spawn(async move {
+            let result = conn.prompt(params).await;
+
+            let mut suppress_abort_err = false;
+
+            if let Some(session) = sessions.borrow_mut().get_mut(&session_id) {
+                suppress_abort_err = session.suppress_abort_err;
+                session.suppress_abort_err = false;
+            }
+
+            match result {
+                Ok(response) => Ok(response),
+                Err(err) => {
+                    if err.code != ErrorCode::INTERNAL_ERROR.code {
+                        anyhow::bail!(err)
+                    }
+
+                    let Some(data) = &err.data else {
+                        anyhow::bail!(err)
+                    };
+
+                    // Temporary workaround until the following PR is generally available:
+                    // https://github.com/google-gemini/gemini-cli/pull/6656
+
+                    #[derive(Deserialize)]
+                    #[serde(deny_unknown_fields)]
+                    struct ErrorDetails {
+                        details: Box<str>,
+                    }
+
+                    match serde_json::from_value(data.clone()) {
+                        Ok(ErrorDetails { details }) => {
+                            if suppress_abort_err && details.contains("This operation was aborted")
+                            {
+                                Ok(acp::PromptResponse {
+                                    stop_reason: acp::StopReason::Cancelled,
+                                })
+                            } else {
+                                Err(anyhow!(details))
+                            }
+                        }
+                        Err(_) => Err(anyhow!(err)),
+                    }
+                }
+            }
+        })
+    }
+
+    fn prompt_capabilities(&self) -> acp::PromptCapabilities {
+        self.prompt_capabilities
+    }
+
+    fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
+        if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
+            session.suppress_abort_err = true;
         }
-        Err(err) => Err(err),
+        let conn = self.connection.clone();
+        let params = acp::CancelNotification {
+            session_id: session_id.clone(),
+        };
+        cx.foreground_executor()
+            .spawn(async move { conn.cancel(params).await })
+            .detach();
+    }
+
+    fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+        self
+    }
+}
+
+struct ClientDelegate {
+    sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
+    cx: AsyncApp,
+}
+
+impl acp::Client for ClientDelegate {
+    async fn request_permission(
+        &self,
+        arguments: acp::RequestPermissionRequest,
+    ) -> Result<acp::RequestPermissionResponse, acp::Error> {
+        let cx = &mut self.cx.clone();
+        let rx = self
+            .sessions
+            .borrow()
+            .get(&arguments.session_id)
+            .context("Failed to get session")?
+            .thread
+            .update(cx, |thread, cx| {
+                thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
+            })?;
+
+        let result = rx?.await;
+
+        let outcome = match result {
+            Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
+            Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
+        };
+
+        Ok(acp::RequestPermissionResponse { outcome })
+    }
+
+    async fn write_text_file(
+        &self,
+        arguments: acp::WriteTextFileRequest,
+    ) -> Result<(), acp::Error> {
+        let cx = &mut self.cx.clone();
+        let task = self
+            .sessions
+            .borrow()
+            .get(&arguments.session_id)
+            .context("Failed to get session")?
+            .thread
+            .update(cx, |thread, cx| {
+                thread.write_text_file(arguments.path, arguments.content, cx)
+            })?;
+
+        task.await?;
+
+        Ok(())
+    }
+
+    async fn read_text_file(
+        &self,
+        arguments: acp::ReadTextFileRequest,
+    ) -> Result<acp::ReadTextFileResponse, acp::Error> {
+        let cx = &mut self.cx.clone();
+        let task = self
+            .sessions
+            .borrow()
+            .get(&arguments.session_id)
+            .context("Failed to get session")?
+            .thread
+            .update(cx, |thread, cx| {
+                thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
+            })?;
+
+        let content = task.await?;
+
+        Ok(acp::ReadTextFileResponse { content })
+    }
+
+    async fn session_notification(
+        &self,
+        notification: acp::SessionNotification,
+    ) -> Result<(), acp::Error> {
+        let cx = &mut self.cx.clone();
+        let sessions = self.sessions.borrow();
+        let session = sessions
+            .get(&notification.session_id)
+            .context("Failed to get session")?;
+
+        session.thread.update(cx, |thread, cx| {
+            thread.handle_session_update(notification.update, cx)
+        })??;
+
+        Ok(())
     }
 }

tooling/workspace-hack/Cargo.toml 🔗

@@ -109,7 +109,6 @@ rustc-hash = { version = "1" }
 rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] }
 rustls = { version = "0.23", features = ["ring"] }
 rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] }
-schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] }
 sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] }
 sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] }
 semver = { version = "1", features = ["serde"] }
@@ -244,7 +243,6 @@ rustc-hash = { version = "1" }
 rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] }
 rustls = { version = "0.23", features = ["ring"] }
 rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] }
-schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] }
 sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] }
 sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] }
 semver = { version = "1", features = ["serde"] }