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