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 raw_output: None,
284 }
285}
286
287fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
288 match icon {
289 acp_old::Icon::FileSearch => acp::ToolKind::Search,
290 acp_old::Icon::Folder => acp::ToolKind::Search,
291 acp_old::Icon::Globe => acp::ToolKind::Search,
292 acp_old::Icon::Hammer => acp::ToolKind::Other,
293 acp_old::Icon::LightBulb => acp::ToolKind::Think,
294 acp_old::Icon::Pencil => acp::ToolKind::Edit,
295 acp_old::Icon::Regex => acp::ToolKind::Search,
296 acp_old::Icon::Terminal => acp::ToolKind::Execute,
297 }
298}
299
300fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
301 match status {
302 acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
303 acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
304 acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
305 }
306}
307
308fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
309 match content {
310 acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
311 acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
312 diff: into_new_diff(diff),
313 },
314 }
315}
316
317fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
318 acp::Diff {
319 path: diff.path,
320 old_text: diff.old_text,
321 new_text: diff.new_text,
322 }
323}
324
325fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
326 acp::ToolCallLocation {
327 path: location.path,
328 line: location.line,
329 }
330}
331
332fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
333 acp::PlanEntry {
334 content: entry.content,
335 priority: into_new_plan_priority(entry.priority),
336 status: into_new_plan_status(entry.status),
337 }
338}
339
340fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
341 match priority {
342 acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
343 acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
344 acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
345 }
346}
347
348fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
349 match status {
350 acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
351 acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
352 acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
353 }
354}
355
356pub struct AcpConnection {
357 pub name: &'static str,
358 pub connection: acp_old::AgentConnection,
359 pub _child_status: Task<Result<()>>,
360 pub current_thread: Rc<RefCell<WeakEntity<AcpThread>>>,
361}
362
363impl AcpConnection {
364 pub fn stdio(
365 name: &'static str,
366 command: AgentServerCommand,
367 root_dir: &Path,
368 cx: &mut AsyncApp,
369 ) -> Task<Result<Self>> {
370 let root_dir = root_dir.to_path_buf();
371
372 cx.spawn(async move |cx| {
373 let mut child = util::command::new_smol_command(&command.path)
374 .args(command.args.iter())
375 .current_dir(root_dir)
376 .stdin(std::process::Stdio::piped())
377 .stdout(std::process::Stdio::piped())
378 .stderr(std::process::Stdio::inherit())
379 .kill_on_drop(true)
380 .spawn()?;
381
382 let stdin = child.stdin.take().unwrap();
383 let stdout = child.stdout.take().unwrap();
384 log::trace!("Spawned (pid: {})", child.id());
385
386 let foreground_executor = cx.foreground_executor().clone();
387
388 let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid()));
389
390 let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent(
391 OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()),
392 stdin,
393 stdout,
394 move |fut| foreground_executor.spawn(fut).detach(),
395 );
396
397 let io_task = cx.background_spawn(async move {
398 io_fut.await.log_err();
399 });
400
401 let child_status = cx.background_spawn(async move {
402 let result = match child.status().await {
403 Err(e) => Err(anyhow!(e)),
404 Ok(result) if result.success() => Ok(()),
405 Ok(result) => Err(anyhow!(result)),
406 };
407 drop(io_task);
408 result
409 });
410
411 Ok(Self {
412 name,
413 connection,
414 _child_status: child_status,
415 current_thread: thread_rc,
416 })
417 })
418 }
419}
420
421impl AgentConnection for AcpConnection {
422 fn new_thread(
423 self: Rc<Self>,
424 project: Entity<Project>,
425 _cwd: &Path,
426 cx: &mut AsyncApp,
427 ) -> Task<Result<Entity<AcpThread>>> {
428 let task = self.connection.request_any(
429 acp_old::InitializeParams {
430 protocol_version: acp_old::ProtocolVersion::latest(),
431 }
432 .into_any(),
433 );
434 let current_thread = self.current_thread.clone();
435 cx.spawn(async move |cx| {
436 let result = task.await?;
437 let result = acp_old::InitializeParams::response_from_any(result)?;
438
439 if !result.is_authenticated {
440 anyhow::bail!(AuthRequired)
441 }
442
443 cx.update(|cx| {
444 let thread = cx.new(|cx| {
445 let session_id = acp::SessionId("acp-old-no-id".into());
446 AcpThread::new(self.name, self.clone(), project, session_id, cx)
447 });
448 current_thread.replace(thread.downgrade());
449 thread
450 })
451 })
452 }
453
454 fn auth_methods(&self) -> &[acp::AuthMethod] {
455 &[]
456 }
457
458 fn authenticate(&self, _method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
459 let task = self
460 .connection
461 .request_any(acp_old::AuthenticateParams.into_any());
462 cx.foreground_executor().spawn(async move {
463 task.await?;
464 Ok(())
465 })
466 }
467
468 fn prompt(
469 &self,
470 _id: Option<acp_thread::UserMessageId>,
471 params: acp::PromptRequest,
472 cx: &mut App,
473 ) -> Task<Result<acp::PromptResponse>> {
474 let chunks = params
475 .prompt
476 .into_iter()
477 .filter_map(|block| match block {
478 acp::ContentBlock::Text(text) => {
479 Some(acp_old::UserMessageChunk::Text { text: text.text })
480 }
481 acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
482 path: link.uri.into(),
483 }),
484 _ => None,
485 })
486 .collect();
487
488 let task = self
489 .connection
490 .request_any(acp_old::SendUserMessageParams { chunks }.into_any());
491 cx.foreground_executor().spawn(async move {
492 task.await?;
493 anyhow::Ok(acp::PromptResponse {
494 stop_reason: acp::StopReason::EndTurn,
495 })
496 })
497 }
498
499 fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
500 let task = self
501 .connection
502 .request_any(acp_old::CancelSendMessageParams.into_any());
503 cx.foreground_executor()
504 .spawn(async move {
505 task.await?;
506 anyhow::Ok(())
507 })
508 .detach_and_log_err(cx)
509 }
510}