1// Translates old acp agents into the new schema
2use agent_client_protocol as acp;
3use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
4use anyhow::{Context as _, Result, anyhow};
5use futures::channel::oneshot;
6use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
7use project::Project;
8use std::{cell::RefCell, path::Path, rc::Rc};
9use ui::App;
10use util::ResultExt as _;
11
12use crate::AgentServerCommand;
13use acp_thread::{AcpThread, AgentConnection, AuthRequired};
14
15#[derive(Clone)]
16struct OldAcpClientDelegate {
17 thread: Rc<RefCell<WeakEntity<AcpThread>>>,
18 cx: AsyncApp,
19 next_tool_call_id: Rc<RefCell<u64>>,
20 // sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
21}
22
23impl OldAcpClientDelegate {
24 fn new(thread: Rc<RefCell<WeakEntity<AcpThread>>>, cx: AsyncApp) -> Self {
25 Self {
26 thread,
27 cx,
28 next_tool_call_id: Rc::new(RefCell::new(0)),
29 }
30 }
31}
32
33impl acp_old::Client for OldAcpClientDelegate {
34 async fn stream_assistant_message_chunk(
35 &self,
36 params: acp_old::StreamAssistantMessageChunkParams,
37 ) -> Result<(), acp_old::Error> {
38 let cx = &mut self.cx.clone();
39
40 cx.update(|cx| {
41 self.thread
42 .borrow()
43 .update(cx, |thread, cx| match params.chunk {
44 acp_old::AssistantMessageChunk::Text { text } => {
45 thread.push_assistant_content_block(text.into(), false, cx)
46 }
47 acp_old::AssistantMessageChunk::Thought { thought } => {
48 thread.push_assistant_content_block(thought.into(), true, cx)
49 }
50 })
51 .log_err();
52 })?;
53
54 Ok(())
55 }
56
57 async fn request_tool_call_confirmation(
58 &self,
59 request: acp_old::RequestToolCallConfirmationParams,
60 ) -> Result<acp_old::RequestToolCallConfirmationResponse, acp_old::Error> {
61 let cx = &mut self.cx.clone();
62
63 let old_acp_id = *self.next_tool_call_id.borrow() + 1;
64 self.next_tool_call_id.replace(old_acp_id);
65
66 let tool_call = into_new_tool_call(
67 acp::ToolCallId(old_acp_id.to_string().into()),
68 request.tool_call,
69 );
70
71 let mut options = match request.confirmation {
72 acp_old::ToolCallConfirmation::Edit { .. } => vec![(
73 acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
74 acp::PermissionOptionKind::AllowAlways,
75 "Always Allow Edits".to_string(),
76 )],
77 acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![(
78 acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
79 acp::PermissionOptionKind::AllowAlways,
80 format!("Always Allow {}", root_command),
81 )],
82 acp_old::ToolCallConfirmation::Mcp {
83 server_name,
84 tool_name,
85 ..
86 } => vec![
87 (
88 acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
89 acp::PermissionOptionKind::AllowAlways,
90 format!("Always Allow {}", server_name),
91 ),
92 (
93 acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool,
94 acp::PermissionOptionKind::AllowAlways,
95 format!("Always Allow {}", tool_name),
96 ),
97 ],
98 acp_old::ToolCallConfirmation::Fetch { .. } => vec![(
99 acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
100 acp::PermissionOptionKind::AllowAlways,
101 "Always Allow".to_string(),
102 )],
103 acp_old::ToolCallConfirmation::Other { .. } => vec![(
104 acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
105 acp::PermissionOptionKind::AllowAlways,
106 "Always Allow".to_string(),
107 )],
108 };
109
110 options.extend([
111 (
112 acp_old::ToolCallConfirmationOutcome::Allow,
113 acp::PermissionOptionKind::AllowOnce,
114 "Allow".to_string(),
115 ),
116 (
117 acp_old::ToolCallConfirmationOutcome::Reject,
118 acp::PermissionOptionKind::RejectOnce,
119 "Reject".to_string(),
120 ),
121 ]);
122
123 let mut outcomes = Vec::with_capacity(options.len());
124 let mut acp_options = Vec::with_capacity(options.len());
125
126 for (index, (outcome, kind, label)) in options.into_iter().enumerate() {
127 outcomes.push(outcome);
128 acp_options.push(acp::PermissionOption {
129 id: acp::PermissionOptionId(index.to_string().into()),
130 name: label,
131 kind,
132 })
133 }
134
135 let response = cx
136 .update(|cx| {
137 self.thread.borrow().update(cx, |thread, cx| {
138 thread.request_tool_call_authorization(tool_call, acp_options, cx)
139 })
140 })?
141 .context("Failed to update thread")?
142 .await;
143
144 let outcome = match response {
145 Ok(option_id) => outcomes[option_id.0.parse::<usize>().unwrap_or(0)],
146 Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel,
147 };
148
149 Ok(acp_old::RequestToolCallConfirmationResponse {
150 id: acp_old::ToolCallId(old_acp_id),
151 outcome: outcome,
152 })
153 }
154
155 async fn push_tool_call(
156 &self,
157 request: acp_old::PushToolCallParams,
158 ) -> Result<acp_old::PushToolCallResponse, acp_old::Error> {
159 let cx = &mut self.cx.clone();
160
161 let old_acp_id = *self.next_tool_call_id.borrow() + 1;
162 self.next_tool_call_id.replace(old_acp_id);
163
164 cx.update(|cx| {
165 self.thread.borrow().update(cx, |thread, cx| {
166 thread.upsert_tool_call(
167 into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request),
168 cx,
169 )
170 })
171 })?
172 .context("Failed to update thread")?;
173
174 Ok(acp_old::PushToolCallResponse {
175 id: acp_old::ToolCallId(old_acp_id),
176 })
177 }
178
179 async fn update_tool_call(
180 &self,
181 request: acp_old::UpdateToolCallParams,
182 ) -> Result<(), acp_old::Error> {
183 let cx = &mut self.cx.clone();
184
185 cx.update(|cx| {
186 self.thread.borrow().update(cx, |thread, cx| {
187 thread.update_tool_call(
188 acp::ToolCallUpdate {
189 id: acp::ToolCallId(request.tool_call_id.0.to_string().into()),
190 fields: acp::ToolCallUpdateFields {
191 status: Some(into_new_tool_call_status(request.status)),
192 content: Some(
193 request
194 .content
195 .into_iter()
196 .map(into_new_tool_call_content)
197 .collect::<Vec<_>>(),
198 ),
199 ..Default::default()
200 },
201 },
202 cx,
203 )
204 })
205 })?
206 .context("Failed to update thread")??;
207
208 Ok(())
209 }
210
211 async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> {
212 let cx = &mut self.cx.clone();
213
214 cx.update(|cx| {
215 self.thread.borrow().update(cx, |thread, cx| {
216 thread.update_plan(
217 acp::Plan {
218 entries: request
219 .entries
220 .into_iter()
221 .map(into_new_plan_entry)
222 .collect(),
223 },
224 cx,
225 )
226 })
227 })?
228 .context("Failed to update thread")?;
229
230 Ok(())
231 }
232
233 async fn read_text_file(
234 &self,
235 acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams,
236 ) -> Result<acp_old::ReadTextFileResponse, acp_old::Error> {
237 let content = self
238 .cx
239 .update(|cx| {
240 self.thread.borrow().update(cx, |thread, cx| {
241 thread.read_text_file(path, line, limit, false, cx)
242 })
243 })?
244 .context("Failed to update thread")?
245 .await?;
246 Ok(acp_old::ReadTextFileResponse { content })
247 }
248
249 async fn write_text_file(
250 &self,
251 acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams,
252 ) -> Result<(), acp_old::Error> {
253 self.cx
254 .update(|cx| {
255 self.thread
256 .borrow()
257 .update(cx, |thread, cx| thread.write_text_file(path, content, cx))
258 })?
259 .context("Failed to update thread")?
260 .await?;
261
262 Ok(())
263 }
264}
265
266fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
267 acp::ToolCall {
268 id: id,
269 title: request.label,
270 kind: acp_kind_from_old_icon(request.icon),
271 status: acp::ToolCallStatus::InProgress,
272 content: request
273 .content
274 .into_iter()
275 .map(into_new_tool_call_content)
276 .collect(),
277 locations: request
278 .locations
279 .into_iter()
280 .map(into_new_tool_call_location)
281 .collect(),
282 raw_input: None,
283 }
284}
285
286fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
287 match icon {
288 acp_old::Icon::FileSearch => acp::ToolKind::Search,
289 acp_old::Icon::Folder => acp::ToolKind::Search,
290 acp_old::Icon::Globe => acp::ToolKind::Search,
291 acp_old::Icon::Hammer => acp::ToolKind::Other,
292 acp_old::Icon::LightBulb => acp::ToolKind::Think,
293 acp_old::Icon::Pencil => acp::ToolKind::Edit,
294 acp_old::Icon::Regex => acp::ToolKind::Search,
295 acp_old::Icon::Terminal => acp::ToolKind::Execute,
296 }
297}
298
299fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
300 match status {
301 acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
302 acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
303 acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
304 }
305}
306
307fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
308 match content {
309 acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
310 acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
311 diff: into_new_diff(diff),
312 },
313 }
314}
315
316fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
317 acp::Diff {
318 path: diff.path,
319 old_text: diff.old_text,
320 new_text: diff.new_text,
321 }
322}
323
324fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
325 acp::ToolCallLocation {
326 path: location.path,
327 line: location.line,
328 }
329}
330
331fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
332 acp::PlanEntry {
333 content: entry.content,
334 priority: into_new_plan_priority(entry.priority),
335 status: into_new_plan_status(entry.status),
336 }
337}
338
339fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
340 match priority {
341 acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
342 acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
343 acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
344 }
345}
346
347fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
348 match status {
349 acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
350 acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
351 acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
352 }
353}
354
355pub struct AcpConnection {
356 pub name: &'static str,
357 pub connection: acp_old::AgentConnection,
358 pub _child_status: Task<Result<()>>,
359 pub current_thread: Rc<RefCell<WeakEntity<AcpThread>>>,
360}
361
362impl AcpConnection {
363 pub fn stdio(
364 name: &'static str,
365 command: AgentServerCommand,
366 root_dir: &Path,
367 cx: &mut AsyncApp,
368 ) -> Task<Result<Self>> {
369 let root_dir = root_dir.to_path_buf();
370
371 cx.spawn(async move |cx| {
372 let mut child = util::command::new_smol_command(&command.path)
373 .args(command.args.iter())
374 .current_dir(root_dir)
375 .stdin(std::process::Stdio::piped())
376 .stdout(std::process::Stdio::piped())
377 .stderr(std::process::Stdio::inherit())
378 .kill_on_drop(true)
379 .spawn()?;
380
381 let stdin = child.stdin.take().unwrap();
382 let stdout = child.stdout.take().unwrap();
383 log::trace!("Spawned (pid: {})", child.id());
384
385 let foreground_executor = cx.foreground_executor().clone();
386
387 let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid()));
388
389 let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent(
390 OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()),
391 stdin,
392 stdout,
393 move |fut| foreground_executor.spawn(fut).detach(),
394 );
395
396 let io_task = cx.background_spawn(async move {
397 io_fut.await.log_err();
398 });
399
400 let child_status = cx.background_spawn(async move {
401 let result = match child.status().await {
402 Err(e) => Err(anyhow!(e)),
403 Ok(result) if result.success() => Ok(()),
404 Ok(result) => Err(anyhow!(result)),
405 };
406 drop(io_task);
407 result
408 });
409
410 Ok(Self {
411 name,
412 connection,
413 _child_status: child_status,
414 current_thread: thread_rc,
415 })
416 })
417 }
418}
419
420impl AgentConnection for AcpConnection {
421 fn new_thread(
422 self: Rc<Self>,
423 project: Entity<Project>,
424 _cwd: &Path,
425 cx: &mut AsyncApp,
426 ) -> Task<Result<Entity<AcpThread>>> {
427 let task = self.connection.request_any(
428 acp_old::InitializeParams {
429 protocol_version: acp_old::ProtocolVersion::latest(),
430 }
431 .into_any(),
432 );
433 let current_thread = self.current_thread.clone();
434 cx.spawn(async move |cx| {
435 let result = task.await?;
436 let result = acp_old::InitializeParams::response_from_any(result)?;
437
438 if !result.is_authenticated {
439 anyhow::bail!(AuthRequired)
440 }
441
442 cx.update(|cx| {
443 let thread = cx.new(|cx| {
444 let session_id = acp::SessionId("acp-old-no-id".into());
445 AcpThread::new(self.name, self.clone(), project, session_id, cx)
446 });
447 current_thread.replace(thread.downgrade());
448 thread
449 })
450 })
451 }
452
453 fn auth_methods(&self) -> &[acp::AuthMethod] {
454 &[]
455 }
456
457 fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
458 let task = self
459 .connection
460 .request_any(acp_old::AuthenticateParams.into_any());
461 cx.foreground_executor().spawn(async move {
462 task.await?;
463 Ok(())
464 })
465 }
466
467 fn prompt(
468 &self,
469 params: acp::PromptRequest,
470 cx: &mut App,
471 ) -> Task<Result<acp::PromptResponse>> {
472 let chunks = params
473 .prompt
474 .into_iter()
475 .filter_map(|block| match block {
476 acp::ContentBlock::Text(text) => {
477 Some(acp_old::UserMessageChunk::Text { text: text.text })
478 }
479 acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
480 path: link.uri.into(),
481 }),
482 _ => None,
483 })
484 .collect();
485
486 let task = self
487 .connection
488 .request_any(acp_old::SendUserMessageParams { chunks }.into_any());
489 cx.foreground_executor().spawn(async move {
490 task.await?;
491 anyhow::Ok(acp::PromptResponse {
492 stop_reason: acp::StopReason::EndTurn,
493 })
494 })
495 }
496
497 fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
498 let task = self
499 .connection
500 .request_any(acp_old::CancelSendMessageParams.into_any());
501 cx.foreground_executor()
502 .spawn(async move {
503 task.await?;
504 anyhow::Ok(())
505 })
506 .detach_and_log_err(cx)
507 }
508}