ACP over MCP server impl (#35196)
Release Notes: - N/A --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This commit is contained in:
parent
b02ae771cd
commit
c2fc70eef7
20 changed files with 899 additions and 137 deletions
|
@ -166,6 +166,7 @@ pub struct ToolCall {
|
|||
pub content: Vec<ToolCallContent>,
|
||||
pub status: ToolCallStatus,
|
||||
pub locations: Vec<acp::ToolCallLocation>,
|
||||
pub raw_input: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl ToolCall {
|
||||
|
@ -193,6 +194,50 @@ impl ToolCall {
|
|||
.collect(),
|
||||
locations: tool_call.locations,
|
||||
status,
|
||||
raw_input: tool_call.raw_input,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(
|
||||
&mut self,
|
||||
fields: acp::ToolCallUpdateFields,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let acp::ToolCallUpdateFields {
|
||||
kind,
|
||||
status,
|
||||
label,
|
||||
content,
|
||||
locations,
|
||||
raw_input,
|
||||
} = fields;
|
||||
|
||||
if let Some(kind) = kind {
|
||||
self.kind = kind;
|
||||
}
|
||||
|
||||
if let Some(status) = status {
|
||||
self.status = ToolCallStatus::Allowed { status };
|
||||
}
|
||||
|
||||
if let Some(label) = label {
|
||||
self.label = cx.new(|cx| Markdown::new_text(label.into(), cx));
|
||||
}
|
||||
|
||||
if let Some(content) = content {
|
||||
self.content = content
|
||||
.into_iter()
|
||||
.map(|chunk| ToolCallContent::from_acp(chunk, language_registry.clone(), cx))
|
||||
.collect();
|
||||
}
|
||||
|
||||
if let Some(locations) = locations {
|
||||
self.locations = locations;
|
||||
}
|
||||
|
||||
if let Some(raw_input) = raw_input {
|
||||
self.raw_input = Some(raw_input);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -238,6 +283,7 @@ impl Display for ToolCallStatus {
|
|||
match self {
|
||||
ToolCallStatus::WaitingForConfirmation { .. } => "Waiting for confirmation",
|
||||
ToolCallStatus::Allowed { status } => match status {
|
||||
acp::ToolCallStatus::Pending => "Pending",
|
||||
acp::ToolCallStatus::InProgress => "In Progress",
|
||||
acp::ToolCallStatus::Completed => "Completed",
|
||||
acp::ToolCallStatus::Failed => "Failed",
|
||||
|
@ -345,7 +391,7 @@ impl ToolCallContent {
|
|||
cx: &mut App,
|
||||
) -> Self {
|
||||
match content {
|
||||
acp::ToolCallContent::ContentBlock { content } => Self::ContentBlock {
|
||||
acp::ToolCallContent::ContentBlock(content) => Self::ContentBlock {
|
||||
content: ContentBlock::new(content, &language_registry, cx),
|
||||
},
|
||||
acp::ToolCallContent::Diff { diff } => Self::Diff {
|
||||
|
@ -630,12 +676,50 @@ impl AcpThread {
|
|||
false
|
||||
}
|
||||
|
||||
pub fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
|
||||
self.entries.push(entry);
|
||||
cx.emit(AcpThreadEvent::NewEntry);
|
||||
pub fn handle_session_update(
|
||||
&mut self,
|
||||
update: acp::SessionUpdate,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
match update {
|
||||
acp::SessionUpdate::UserMessage(content_block) => {
|
||||
self.push_user_content_block(content_block, cx);
|
||||
}
|
||||
acp::SessionUpdate::AgentMessageChunk(content_block) => {
|
||||
self.push_assistant_content_block(content_block, false, cx);
|
||||
}
|
||||
acp::SessionUpdate::AgentThoughtChunk(content_block) => {
|
||||
self.push_assistant_content_block(content_block, true, cx);
|
||||
}
|
||||
acp::SessionUpdate::ToolCall(tool_call) => {
|
||||
self.upsert_tool_call(tool_call, cx);
|
||||
}
|
||||
acp::SessionUpdate::ToolCallUpdate(tool_call_update) => {
|
||||
self.update_tool_call(tool_call_update, cx)?;
|
||||
}
|
||||
acp::SessionUpdate::Plan(plan) => {
|
||||
self.update_plan(plan, cx);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn push_assistant_chunk(
|
||||
pub fn push_user_content_block(&mut self, chunk: acp::ContentBlock, cx: &mut Context<Self>) {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
let entries_len = self.entries.len();
|
||||
|
||||
if let Some(last_entry) = self.entries.last_mut()
|
||||
&& let AgentThreadEntry::UserMessage(UserMessage { content }) = last_entry
|
||||
{
|
||||
content.append(chunk, &language_registry, cx);
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1));
|
||||
} else {
|
||||
let content = ContentBlock::new(chunk, &language_registry, cx);
|
||||
self.push_entry(AgentThreadEntry::UserMessage(UserMessage { content }), cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_assistant_content_block(
|
||||
&mut self,
|
||||
chunk: acp::ContentBlock,
|
||||
is_thought: bool,
|
||||
|
@ -678,23 +762,22 @@ impl AcpThread {
|
|||
}
|
||||
}
|
||||
|
||||
fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
|
||||
self.entries.push(entry);
|
||||
cx.emit(AcpThreadEvent::NewEntry);
|
||||
}
|
||||
|
||||
pub fn update_tool_call(
|
||||
&mut self,
|
||||
id: acp::ToolCallId,
|
||||
status: acp::ToolCallStatus,
|
||||
content: Option<Vec<acp::ToolCallContent>>,
|
||||
update: acp::ToolCallUpdate,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
let languages = self.project.read(cx).languages().clone();
|
||||
let (ix, current_call) = self.tool_call_mut(&id).context("Tool call not found")?;
|
||||
|
||||
if let Some(content) = content {
|
||||
current_call.content = content
|
||||
.into_iter()
|
||||
.map(|chunk| ToolCallContent::from_acp(chunk, languages.clone(), cx))
|
||||
.collect();
|
||||
}
|
||||
current_call.status = ToolCallStatus::Allowed { status };
|
||||
let (ix, current_call) = self
|
||||
.tool_call_mut(&update.id)
|
||||
.context("Tool call not found")?;
|
||||
current_call.update(update.fields, languages, cx);
|
||||
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
||||
|
||||
|
@ -751,6 +834,37 @@ impl AcpThread {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn set_project_location(&self, location: acp::ToolCallLocation, cx: &mut Context<Self>) {
|
||||
self.project.update(cx, |project, cx| {
|
||||
let Some(path) = project.project_path_for_absolute_path(&location.path, cx) else {
|
||||
return;
|
||||
};
|
||||
let buffer = project.open_buffer(path, cx);
|
||||
cx.spawn(async move |project, cx| {
|
||||
let buffer = buffer.await?;
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
let position = if let Some(line) = location.line {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let point = snapshot.clip_point(Point::new(line, 0), Bias::Left);
|
||||
snapshot.anchor_before(point)
|
||||
} else {
|
||||
Anchor::MIN
|
||||
};
|
||||
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn request_tool_call_permission(
|
||||
&mut self,
|
||||
tool_call: acp::ToolCall,
|
||||
|
@ -801,6 +915,25 @@ impl AcpThread {
|
|||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
||||
}
|
||||
|
||||
/// Returns true if the last turn is awaiting tool authorization
|
||||
pub fn waiting_for_tool_confirmation(&self) -> bool {
|
||||
for entry in self.entries.iter().rev() {
|
||||
match &entry {
|
||||
AgentThreadEntry::ToolCall(call) => match call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { .. } => return true,
|
||||
ToolCallStatus::Allowed { .. }
|
||||
| ToolCallStatus::Rejected
|
||||
| ToolCallStatus::Canceled => continue,
|
||||
},
|
||||
AgentThreadEntry::UserMessage(_) | AgentThreadEntry::AssistantMessage(_) => {
|
||||
// Reached the beginning of the turn
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn plan(&self) -> &Plan {
|
||||
&self.plan
|
||||
}
|
||||
|
@ -824,56 +957,6 @@ impl AcpThread {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_project_location(&self, location: acp::ToolCallLocation, cx: &mut Context<Self>) {
|
||||
self.project.update(cx, |project, cx| {
|
||||
let Some(path) = project.project_path_for_absolute_path(&location.path, cx) else {
|
||||
return;
|
||||
};
|
||||
let buffer = project.open_buffer(path, cx);
|
||||
cx.spawn(async move |project, cx| {
|
||||
let buffer = buffer.await?;
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
let position = if let Some(line) = location.line {
|
||||
let snapshot = buffer.read(cx).snapshot();
|
||||
let point = snapshot.clip_point(Point::new(line, 0), Bias::Left);
|
||||
snapshot.anchor_before(point)
|
||||
} else {
|
||||
Anchor::MIN
|
||||
};
|
||||
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns true if the last turn is awaiting tool authorization
|
||||
pub fn waiting_for_tool_confirmation(&self) -> bool {
|
||||
for entry in self.entries.iter().rev() {
|
||||
match &entry {
|
||||
AgentThreadEntry::ToolCall(call) => match call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { .. } => return true,
|
||||
ToolCallStatus::Allowed { .. }
|
||||
| ToolCallStatus::Rejected
|
||||
| ToolCallStatus::Canceled => continue,
|
||||
},
|
||||
AgentThreadEntry::UserMessage(_) | AgentThreadEntry::AssistantMessage(_) => {
|
||||
// Reached the beginning of the turn
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn authenticate(&self, cx: &mut App) -> impl use<> + Future<Output = Result<()>> {
|
||||
self.connection.authenticate(cx)
|
||||
}
|
||||
|
@ -919,7 +1002,7 @@ impl AcpThread {
|
|||
let result = this
|
||||
.update(cx, |this, cx| {
|
||||
this.connection.prompt(
|
||||
acp::PromptToolArguments {
|
||||
acp::PromptArguments {
|
||||
prompt: message,
|
||||
session_id: this.session_id.clone(),
|
||||
},
|
||||
|
@ -1148,7 +1231,87 @@ mod tests {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thinking_concatenation(cx: &mut TestAppContext) {
|
||||
async fn test_push_user_content_block(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (thread, _fake_server) = fake_acp_thread(project, cx);
|
||||
|
||||
// Test creating a new user message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_user_content_block(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "Hello, ".to_string(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
assert_eq!(thread.entries.len(), 1);
|
||||
if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] {
|
||||
assert_eq!(user_msg.content.to_markdown(cx), "Hello, ");
|
||||
} else {
|
||||
panic!("Expected UserMessage");
|
||||
}
|
||||
});
|
||||
|
||||
// Test appending to existing user message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_user_content_block(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "world!".to_string(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
assert_eq!(thread.entries.len(), 1);
|
||||
if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] {
|
||||
assert_eq!(user_msg.content.to_markdown(cx), "Hello, world!");
|
||||
} else {
|
||||
panic!("Expected UserMessage");
|
||||
}
|
||||
});
|
||||
|
||||
// Test creating new user message after assistant message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_assistant_content_block(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "Assistant response".to_string(),
|
||||
}),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_user_content_block(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "New user message".to_string(),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
assert_eq!(thread.entries.len(), 3);
|
||||
if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[2] {
|
||||
assert_eq!(user_msg.content.to_markdown(cx), "New user message");
|
||||
} else {
|
||||
panic!("Expected UserMessage at index 2");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thinking_concatenation(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
|
|
@ -20,7 +20,7 @@ pub trait AgentConnection {
|
|||
|
||||
fn authenticate(&self, cx: &mut App) -> Task<Result<()>>;
|
||||
|
||||
fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task<Result<()>>;
|
||||
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<Result<()>>;
|
||||
|
||||
fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ use project::Project;
|
|||
use std::{cell::RefCell, error::Error, fmt, path::Path, rc::Rc};
|
||||
use ui::App;
|
||||
|
||||
use crate::{AcpThread, AcpThreadEvent, AgentConnection, ToolCallContent, ToolCallStatus};
|
||||
use crate::{AcpThread, AgentConnection};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OldAcpClientDelegate {
|
||||
|
@ -40,10 +40,10 @@ impl acp_old::Client for OldAcpClientDelegate {
|
|||
.borrow()
|
||||
.update(cx, |thread, cx| match params.chunk {
|
||||
acp_old::AssistantMessageChunk::Text { text } => {
|
||||
thread.push_assistant_chunk(text.into(), false, cx)
|
||||
thread.push_assistant_content_block(text.into(), false, cx)
|
||||
}
|
||||
acp_old::AssistantMessageChunk::Thought { thought } => {
|
||||
thread.push_assistant_chunk(thought.into(), true, cx)
|
||||
thread.push_assistant_content_block(thought.into(), true, cx)
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
|
@ -182,31 +182,23 @@ impl acp_old::Client for OldAcpClientDelegate {
|
|||
|
||||
cx.update(|cx| {
|
||||
self.thread.borrow().update(cx, |thread, cx| {
|
||||
let languages = thread.project.read(cx).languages().clone();
|
||||
|
||||
if let Some((ix, tool_call)) = thread
|
||||
.tool_call_mut(&acp::ToolCallId(request.tool_call_id.0.to_string().into()))
|
||||
{
|
||||
tool_call.status = ToolCallStatus::Allowed {
|
||||
status: into_new_tool_call_status(request.status),
|
||||
};
|
||||
tool_call.content = request
|
||||
.content
|
||||
.into_iter()
|
||||
.map(|content| {
|
||||
ToolCallContent::from_acp(
|
||||
into_new_tool_call_content(content),
|
||||
languages.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
||||
anyhow::Ok(())
|
||||
} else {
|
||||
anyhow::bail!("Tool call not found")
|
||||
}
|
||||
thread.update_tool_call(
|
||||
acp::ToolCallUpdate {
|
||||
id: acp::ToolCallId(request.tool_call_id.0.to_string().into()),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(into_new_tool_call_status(request.status)),
|
||||
content: Some(
|
||||
request
|
||||
.content
|
||||
.into_iter()
|
||||
.map(into_new_tool_call_content)
|
||||
.collect::<Vec<_>>(),
|
||||
),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")??;
|
||||
|
@ -285,6 +277,7 @@ fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams)
|
|||
.into_iter()
|
||||
.map(into_new_tool_call_location)
|
||||
.collect(),
|
||||
raw_input: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -311,12 +304,7 @@ fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallSt
|
|||
|
||||
fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
|
||||
match content {
|
||||
acp_old::ToolCallContent::Markdown { markdown } => acp::ToolCallContent::ContentBlock {
|
||||
content: acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: markdown,
|
||||
}),
|
||||
},
|
||||
acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
|
||||
acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
|
||||
diff: into_new_diff(diff),
|
||||
},
|
||||
|
@ -423,7 +411,7 @@ impl AgentConnection for OldAcpAgentConnection {
|
|||
})
|
||||
}
|
||||
|
||||
fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task<Result<()>> {
|
||||
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<Result<()>> {
|
||||
let chunks = params
|
||||
.prompt
|
||||
.into_iter()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue