History

acp: Remove ACP v0 (#36785)

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

Conrad Irwin committed 1 year ago

Commit 5da31fd1 parent f48a8f25 files · +382 −72Browse files

Unified · Split

5 files
  1. Cargo.lock +9 −52
  2. Cargo.toml +0 −1
  3. crates/agent_servers/Cargo.toml +0 −1
  4. crates/agent_servers/src/acp.rs +373 −16
  5. tooling/workspace-hack/Cargo.toml +0 −2
Cargo.lock+9−52
@@ -289,7 +289,6 @@ dependencies = [ · 288 unmodified lines
289289 "action_log",
290290 "agent-client-protocol",
291291 "agent_settings",
292− "agentic-coding-protocol",
293292 "anyhow",
294293 "client",
295294 "collections",
@@ -443,24 +442,6 @@ dependencies = [ · 147 unmodified lines
443442 "zed_actions",
444443 ]
445444
446−[[package]]
447−name = "agentic-coding-protocol"
448−version = "0.0.10"
449−source = "registry+https://github.com/rust-lang/crates.io-index"
450−checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4"
451−dependencies = [
452− "anyhow",
453− "chrono",
454− "derive_more 2.0.1",
455− "futures 0.3.31",
456− "log",
457− "parking_lot",
458− "schemars",
459− "semver",
460− "serde",
461− "serde_json",
462−]
463−
464445 [[package]]
465446 name = "ahash"
466447 version = "0.7.8"
@@ -876,7 +857,7 @@ dependencies = [ · 409 unmodified lines
876857 "anyhow",
877858 "async-trait",
878859 "collections",
879− "derive_more 0.99.19",
860+ "derive_more",
880861 "extension",
881862 "futures 0.3.31",
882863 "gpui",
@@ -939,7 +920,7 @@ dependencies = [ · 56 unmodified lines
939920 "clock",
940921 "collections",
941922 "ctor",
942− "derive_more 0.99.19",
923+ "derive_more",
943924 "gpui",
944925 "icons",
945926 "indoc",
@@ -976,7 +957,7 @@ dependencies = [ · 30 unmodified lines
976957 "cloud_llm_client",
977958 "collections",
978959 "component",
979− "derive_more 0.99.19",
960+ "derive_more",
980961 "diffy",
981962 "editor",
982963 "feature_flags",
@@ -3089,7 +3070,7 @@ dependencies = [ · 2106 unmodified lines
30893070 "cocoa 0.26.0",
30903071 "collections",
30913072 "credentials_provider",
3092− "derive_more 0.99.19",
3073+ "derive_more",
30933074 "feature_flags",
30943075 "fs",
30953076 "futures 0.3.31",
@@ -3521,7 +3502,7 @@ name = "command_palette_hooks" · 425 unmodified lines
35213502 version = "0.1.0"
35223503 dependencies = [
35233504 "collections",
3524− "derive_more 0.99.19",
3505+ "derive_more",
35253506 "gpui",
35263507 "workspace-hack",
35273508 ]
@@ -4684,27 +4665,6 @@ dependencies = [ · 1156 unmodified lines
46844665 "syn 2.0.101",
46854666 ]
46864667
4687−[[package]]
4688−name = "derive_more"
4689−version = "2.0.1"
4690−source = "registry+https://github.com/rust-lang/crates.io-index"
4691−checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
4692−dependencies = [
4693− "derive_more-impl",
4694−]
4695−
4696−[[package]]
4697−name = "derive_more-impl"
4698−version = "2.0.1"
4699−source = "registry+https://github.com/rust-lang/crates.io-index"
4700−checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
4701−dependencies = [
4702− "proc-macro2",
4703− "quote",
4704− "syn 2.0.101",
4705− "unicode-xid",
4706−]
4707−
47084668 [[package]]
47094669 name = "derive_refineable"
47104670 version = "0.1.0"
@@ -6441,7 +6401,7 @@ dependencies = [ · 1730 unmodified lines
64416401 "askpass",
64426402 "async-trait",
64436403 "collections",
6444− "derive_more 0.99.19",
6404+ "derive_more",
64456405 "futures 0.3.31",
64466406 "git2",
64476407 "gpui",
@@ -7471,7 +7431,7 @@ dependencies = [ · 1023 unmodified lines
74717431 "core-video",
74727432 "cosmic-text",
74737433 "ctor",
7474− "derive_more 0.99.19",
7434+ "derive_more",
74757435 "embed-resource",
74767436 "env_logger 0.11.8",
74777437 "etagere",
@@ -7996,7 +7956,7 @@ version = "0.1.0" · 518 unmodified lines
79967956 dependencies = [
79977957 "anyhow",
79987958 "bytes 1.10.1",
7999− "derive_more 0.99.19",
7959+ "derive_more",
80007960 "futures 0.3.31",
80017961 "http 1.3.1",
80027962 "http-body 1.0.1",
@@ -14399,12 +14359,10 @@ version = "1.0.1" · 6396 unmodified lines
1439914359 source = "registry+https://github.com/rust-lang/crates.io-index"
1440014360 checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
1440114361 dependencies = [
14402− "chrono",
1440314362 "dyn-clone",
1440414363 "indexmap",
1440514364 "ref-cast",
1440614365 "schemars_derive",
14407− "semver",
1440814366 "serde",
1440914367 "serde_json",
1441014368 ]
@@ -16488,7 +16446,7 @@ version = "0.1.0" · 2077 unmodified lines
1648816446 dependencies = [
1648916447 "anyhow",
1649016448 "collections",
16491− "derive_more 0.99.19",
16449+ "derive_more",
1649216450 "fs",
1649316451 "futures 0.3.31",
1649416452 "gpui",
@@ -20003,7 +19961,6 @@ dependencies = [ · 3508 unmodified lines
2000319961 "rustix 1.0.7",
2000419962 "rustls 0.23.26",
2000519963 "rustls-webpki 0.103.1",
20006− "schemars",
2000719964 "scopeguard",
2000819965 "sea-orm",
2000919966 "sea-query-binder",
Cargo.toml+0−1
@@ -426,7 +426,6 @@ zlog_settings = { path = "crates/zlog_settings" } · 425 unmodified lines
426426 # External crates
427427 #
428428
429−agentic-coding-protocol = "0.0.10"
430429 agent-client-protocol = "0.0.31"
431430 aho-corasick = "1.1"
432431 alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
crates/agent_servers/Cargo.toml+0−1
@@ -22,7 +22,6 @@ acp_thread.workspace = true · 21 unmodified lines
2222 action_log.workspace = true
2323 agent-client-protocol.workspace = true
2424 agent_settings.workspace = true
25−agentic-coding-protocol.workspace = true
2625 anyhow.workspace = true
2726 client = { workspace = true, optional = true }
2827 collections.workspace = true
crates/agent_servers/src/acp.rs+373−16
@@ -1,34 +1,391 @@
1−use std::{path::Path, rc::Rc};
2−
31 use crate::AgentServerCommand;
42 use acp_thread::AgentConnection;
5−use anyhow::Result;
6−use gpui::AsyncApp;
3+use acp_tools::AcpConnectionRegistry;
4+use action_log::ActionLog;
5+use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
6+use anyhow::anyhow;
7+use collections::HashMap;
8+use futures::AsyncBufReadExt as _;
9+use futures::channel::oneshot;
10+use futures::io::BufReader;
11+use project::Project;
12+use serde::Deserialize;
13+use std::{any::Any, cell::RefCell};
14+use std::{path::Path, rc::Rc};
715 use thiserror::Error;
816
9−mod v0;
10−mod v1;
17+use anyhow::{Context as _, Result};
18+use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
19+
20+use acp_thread::{AcpThread, AuthRequired, LoadError};
1121
1222 #[derive(Debug, Error)]
1323 #[error("Unsupported version")]
1424 pub struct UnsupportedVersion;
1525
26+pub struct AcpConnection {
27+ server_name: &'static str,
28+ connection: Rc<acp::ClientSideConnection>,
29+ sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
30+ auth_methods: Vec<acp::AuthMethod>,
31+ prompt_capabilities: acp::PromptCapabilities,
32+ _io_task: Task<Result<()>>,
33+}
34+
35+pub struct AcpSession {
36+ thread: WeakEntity<AcpThread>,
37+ suppress_abort_err: bool,
38+}
39+
1640 pub async fn connect(
1741 server_name: &'static str,
1842 command: AgentServerCommand,
1943 root_dir: &Path,
2044 cx: &mut AsyncApp,
2145 ) -> Result<Rc<dyn AgentConnection>> {
22− let conn = v1::AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await;
23−
24− match conn {
25− Ok(conn) => Ok(Rc::new(conn) as _),
26− Err(err) if err.is::<UnsupportedVersion>() => {
27− // Consider re-using initialize response and subprocess when adding another version here
28− let conn: Rc<dyn AgentConnection> =
29− Rc::new(v0::AcpConnection::stdio(server_name, command, root_dir, cx).await?);
30− Ok(conn)
46+ let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?;
47+ Ok(Rc::new(conn) as _)
48+}
49+
50+const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
51+
52+impl AcpConnection {
53+ pub async fn stdio(
54+ server_name: &'static str,
55+ command: AgentServerCommand,
56+ root_dir: &Path,
57+ cx: &mut AsyncApp,
58+ ) -> Result<Self> {
59+ let mut child = util::command::new_smol_command(&command.path)
60+ .args(command.args.iter().map(|arg| arg.as_str()))
61+ .envs(command.env.iter().flatten())
62+ .current_dir(root_dir)
63+ .stdin(std::process::Stdio::piped())
64+ .stdout(std::process::Stdio::piped())
65+ .stderr(std::process::Stdio::piped())
66+ .kill_on_drop(true)
67+ .spawn()?;
68+
69+ let stdout = child.stdout.take().context("Failed to take stdout")?;
70+ let stdin = child.stdin.take().context("Failed to take stdin")?;
71+ let stderr = child.stderr.take().context("Failed to take stderr")?;
72+ log::trace!("Spawned (pid: {})", child.id());
73+
74+ let sessions = Rc::new(RefCell::new(HashMap::default()));
75+
76+ let client = ClientDelegate {
77+ sessions: sessions.clone(),
78+ cx: cx.clone(),
79+ };
80+ let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, {
81+ let foreground_executor = cx.foreground_executor().clone();
82+ move |fut| {
83+ foreground_executor.spawn(fut).detach();
84+ }
85+ });
86+
87+ let io_task = cx.background_spawn(io_task);
88+
89+ cx.background_spawn(async move {
90+ let mut stderr = BufReader::new(stderr);
91+ let mut line = String::new();
92+ while let Ok(n) = stderr.read_line(&mut line).await
93+ && n > 0
94+ {
95+ log::warn!("agent stderr: {}", &line);
96+ line.clear();
97+ }
98+ })
99+ .detach();
100+
101+ cx.spawn({
102+ let sessions = sessions.clone();
103+ async move |cx| {
104+ let status = child.status().await?;
105+
106+ for session in sessions.borrow().values() {
107+ session
108+ .thread
109+ .update(cx, |thread, cx| {
110+ thread.emit_load_error(LoadError::Exited { status }, cx)
111+ })
112+ .ok();
113+ }
114+
115+ anyhow::Ok(())
116+ }
117+ })
118+ .detach();
119+
120+ let connection = Rc::new(connection);
121+
122+ cx.update(|cx| {
123+ AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
124+ registry.set_active_connection(server_name, &connection, cx)
125+ });
126+ })?;
127+
128+ let response = connection
129+ .initialize(acp::InitializeRequest {
130+ protocol_version: acp::VERSION,
131+ client_capabilities: acp::ClientCapabilities {
132+ fs: acp::FileSystemCapability {
133+ read_text_file: true,
134+ write_text_file: true,
135+ },
136+ },
137+ })
138+ .await?;
139+
140+ if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
141+ return Err(UnsupportedVersion.into());
142+ }
143+
144+ Ok(Self {
145+ auth_methods: response.auth_methods,
146+ connection,
147+ server_name,
148+ sessions,
149+ prompt_capabilities: response.agent_capabilities.prompt_capabilities,
150+ _io_task: io_task,
151+ })
152+ }
153+}
154+
155+impl AgentConnection for AcpConnection {
156+ fn new_thread(
157+ self: Rc<Self>,
158+ project: Entity<Project>,
159+ cwd: &Path,
160+ cx: &mut App,
161+ ) -> Task<Result<Entity<AcpThread>>> {
162+ let conn = self.connection.clone();
163+ let sessions = self.sessions.clone();
164+ let cwd = cwd.to_path_buf();
165+ cx.spawn(async move |cx| {
166+ let response = conn
167+ .new_session(acp::NewSessionRequest {
168+ mcp_servers: vec![],
169+ cwd,
170+ })
171+ .await
172+ .map_err(|err| {
173+ if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
174+ let mut error = AuthRequired::new();
175+
176+ if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
177+ error = error.with_description(err.message);
178+ }
179+
180+ anyhow!(error)
181+ } else {
182+ anyhow!(err)
183+ }
184+ })?;
185+
186+ let session_id = response.session_id;
187+ let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
188+ let thread = cx.new(|_cx| {
189+ AcpThread::new(
190+ self.server_name,
191+ self.clone(),
192+ project,
193+ action_log,
194+ session_id.clone(),
195+ )
196+ })?;
197+
198+ let session = AcpSession {
199+ thread: thread.downgrade(),
200+ suppress_abort_err: false,
201+ };
202+ sessions.borrow_mut().insert(session_id, session);
203+
204+ Ok(thread)
205+ })
206+ }
207+
208+ fn auth_methods(&self) -> &[acp::AuthMethod] {
209+ &self.auth_methods
210+ }
211+
212+ fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
213+ let conn = self.connection.clone();
214+ cx.foreground_executor().spawn(async move {
215+ let result = conn
216+ .authenticate(acp::AuthenticateRequest {
217+ method_id: method_id.clone(),
218+ })
219+ .await?;
220+
221+ Ok(result)
222+ })
223+ }
224+
225+ fn prompt(
226+ &self,
227+ _id: Option<acp_thread::UserMessageId>,
228+ params: acp::PromptRequest,
229+ cx: &mut App,
230+ ) -> Task<Result<acp::PromptResponse>> {
231+ let conn = self.connection.clone();
232+ let sessions = self.sessions.clone();
233+ let session_id = params.session_id.clone();
234+ cx.foreground_executor().spawn(async move {
235+ let result = conn.prompt(params).await;
236+
237+ let mut suppress_abort_err = false;
238+
239+ if let Some(session) = sessions.borrow_mut().get_mut(&session_id) {
240+ suppress_abort_err = session.suppress_abort_err;
241+ session.suppress_abort_err = false;
242+ }
243+
244+ match result {
245+ Ok(response) => Ok(response),
246+ Err(err) => {
247+ if err.code != ErrorCode::INTERNAL_ERROR.code {
248+ anyhow::bail!(err)
249+ }
250+
251+ let Some(data) = &err.data else {
252+ anyhow::bail!(err)
253+ };
254+
255+ // Temporary workaround until the following PR is generally available:
256+ // https://github.com/google-gemini/gemini-cli/pull/6656
257+
258+ #[derive(Deserialize)]
259+ #[serde(deny_unknown_fields)]
260+ struct ErrorDetails {
261+ details: Box<str>,
262+ }
263+
264+ match serde_json::from_value(data.clone()) {
265+ Ok(ErrorDetails { details }) => {
266+ if suppress_abort_err && details.contains("This operation was aborted")
267+ {
268+ Ok(acp::PromptResponse {
269+ stop_reason: acp::StopReason::Cancelled,
270+ })
271+ } else {
272+ Err(anyhow!(details))
273+ }
274+ }
275+ Err(_) => Err(anyhow!(err)),
276+ }
277+ }
278+ }
279+ })
280+ }
281+
282+ fn prompt_capabilities(&self) -> acp::PromptCapabilities {
283+ self.prompt_capabilities
284+ }
285+
286+ fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
287+ if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
288+ session.suppress_abort_err = true;
31289 }
32− Err(err) => Err(err),
290+ let conn = self.connection.clone();
291+ let params = acp::CancelNotification {
292+ session_id: session_id.clone(),
293+ };
294+ cx.foreground_executor()
295+ .spawn(async move { conn.cancel(params).await })
296+ .detach();
297+ }
298+
299+ fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
300+ self
301+ }
302+}
303+
304+struct ClientDelegate {
305+ sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
306+ cx: AsyncApp,
307+}
308+
309+impl acp::Client for ClientDelegate {
310+ async fn request_permission(
311+ &self,
312+ arguments: acp::RequestPermissionRequest,
313+ ) -> Result<acp::RequestPermissionResponse, acp::Error> {
314+ let cx = &mut self.cx.clone();
315+ let rx = self
316+ .sessions
317+ .borrow()
318+ .get(&arguments.session_id)
319+ .context("Failed to get session")?
320+ .thread
321+ .update(cx, |thread, cx| {
322+ thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
323+ })?;
324+
325+ let result = rx?.await;
326+
327+ let outcome = match result {
328+ Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
329+ Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
330+ };
331+
332+ Ok(acp::RequestPermissionResponse { outcome })
333+ }
334+
335+ async fn write_text_file(
336+ &self,
337+ arguments: acp::WriteTextFileRequest,
338+ ) -> Result<(), acp::Error> {
339+ let cx = &mut self.cx.clone();
340+ let task = self
341+ .sessions
342+ .borrow()
343+ .get(&arguments.session_id)
344+ .context("Failed to get session")?
345+ .thread
346+ .update(cx, |thread, cx| {
347+ thread.write_text_file(arguments.path, arguments.content, cx)
348+ })?;
349+
350+ task.await?;
351+
352+ Ok(())
353+ }
354+
355+ async fn read_text_file(
356+ &self,
357+ arguments: acp::ReadTextFileRequest,
358+ ) -> Result<acp::ReadTextFileResponse, acp::Error> {
359+ let cx = &mut self.cx.clone();
360+ let task = self
361+ .sessions
362+ .borrow()
363+ .get(&arguments.session_id)
364+ .context("Failed to get session")?
365+ .thread
366+ .update(cx, |thread, cx| {
367+ thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
368+ })?;
369+
370+ let content = task.await?;
371+
372+ Ok(acp::ReadTextFileResponse { content })
373+ }
374+
375+ async fn session_notification(
376+ &self,
377+ notification: acp::SessionNotification,
378+ ) -> Result<(), acp::Error> {
379+ let cx = &mut self.cx.clone();
380+ let sessions = self.sessions.borrow();
381+ let session = sessions
382+ .get(&notification.session_id)
383+ .context("Failed to get session")?;
384+
385+ session.thread.update(cx, |thread, cx| {
386+ thread.handle_session_update(notification.update, cx)
387+ })??;
388+
389+ Ok(())
33390 }
34391 }
tooling/workspace-hack/Cargo.toml+0−2
@@ -109,7 +109,6 @@ rustc-hash = { version = "1" } · 108 unmodified lines
109109 rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] }
110110 rustls = { version = "0.23", features = ["ring"] }
111111 rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] }
112−schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] }
113112 sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] }
114113 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"] }
115114 semver = { version = "1", features = ["serde"] }
@@ -244,7 +243,6 @@ rustc-hash = { version = "1" } · 128 unmodified lines
244243 rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] }
245244 rustls = { version = "0.23", features = ["ring"] }
246245 rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] }
247−schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] }
248246 sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] }
249247 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"] }
250248 semver = { version = "1", features = ["serde"] }