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
4
Cargo.lock
generated
4
Cargo.lock
generated
|
@ -138,9 +138,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "agent-client-protocol"
|
name = "agent-client-protocol"
|
||||||
version = "0.0.10"
|
version = "0.0.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7fb7f39671e02f8a1aeb625652feae40b6fc2597baaa97e028a98863477aecbd"
|
checksum = "72ec54650c1fc2d63498bab47eeeaa9eddc7d239d53f615b797a0e84f7ccc87b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
|
@ -413,7 +413,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||||
#
|
#
|
||||||
|
|
||||||
agentic-coding-protocol = "0.0.10"
|
agentic-coding-protocol = "0.0.10"
|
||||||
agent-client-protocol = "0.0.10"
|
agent-client-protocol = "0.0.11"
|
||||||
aho-corasick = "1.1"
|
aho-corasick = "1.1"
|
||||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||||
any_vec = "0.14"
|
any_vec = "0.14"
|
||||||
|
|
|
@ -166,6 +166,7 @@ pub struct ToolCall {
|
||||||
pub content: Vec<ToolCallContent>,
|
pub content: Vec<ToolCallContent>,
|
||||||
pub status: ToolCallStatus,
|
pub status: ToolCallStatus,
|
||||||
pub locations: Vec<acp::ToolCallLocation>,
|
pub locations: Vec<acp::ToolCallLocation>,
|
||||||
|
pub raw_input: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToolCall {
|
impl ToolCall {
|
||||||
|
@ -193,6 +194,50 @@ impl ToolCall {
|
||||||
.collect(),
|
.collect(),
|
||||||
locations: tool_call.locations,
|
locations: tool_call.locations,
|
||||||
status,
|
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 {
|
match self {
|
||||||
ToolCallStatus::WaitingForConfirmation { .. } => "Waiting for confirmation",
|
ToolCallStatus::WaitingForConfirmation { .. } => "Waiting for confirmation",
|
||||||
ToolCallStatus::Allowed { status } => match status {
|
ToolCallStatus::Allowed { status } => match status {
|
||||||
|
acp::ToolCallStatus::Pending => "Pending",
|
||||||
acp::ToolCallStatus::InProgress => "In Progress",
|
acp::ToolCallStatus::InProgress => "In Progress",
|
||||||
acp::ToolCallStatus::Completed => "Completed",
|
acp::ToolCallStatus::Completed => "Completed",
|
||||||
acp::ToolCallStatus::Failed => "Failed",
|
acp::ToolCallStatus::Failed => "Failed",
|
||||||
|
@ -345,7 +391,7 @@ impl ToolCallContent {
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
match content {
|
match content {
|
||||||
acp::ToolCallContent::ContentBlock { content } => Self::ContentBlock {
|
acp::ToolCallContent::ContentBlock(content) => Self::ContentBlock {
|
||||||
content: ContentBlock::new(content, &language_registry, cx),
|
content: ContentBlock::new(content, &language_registry, cx),
|
||||||
},
|
},
|
||||||
acp::ToolCallContent::Diff { diff } => Self::Diff {
|
acp::ToolCallContent::Diff { diff } => Self::Diff {
|
||||||
|
@ -630,12 +676,50 @@ impl AcpThread {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context<Self>) {
|
pub fn handle_session_update(
|
||||||
self.entries.push(entry);
|
&mut self,
|
||||||
cx.emit(AcpThreadEvent::NewEntry);
|
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,
|
&mut self,
|
||||||
chunk: acp::ContentBlock,
|
chunk: acp::ContentBlock,
|
||||||
is_thought: bool,
|
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(
|
pub fn update_tool_call(
|
||||||
&mut self,
|
&mut self,
|
||||||
id: acp::ToolCallId,
|
update: acp::ToolCallUpdate,
|
||||||
status: acp::ToolCallStatus,
|
|
||||||
content: Option<Vec<acp::ToolCallContent>>,
|
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let languages = self.project.read(cx).languages().clone();
|
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 {
|
let (ix, current_call) = self
|
||||||
current_call.content = content
|
.tool_call_mut(&update.id)
|
||||||
.into_iter()
|
.context("Tool call not found")?;
|
||||||
.map(|chunk| ToolCallContent::from_acp(chunk, languages.clone(), cx))
|
current_call.update(update.fields, languages, cx);
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
current_call.status = ToolCallStatus::Allowed { status };
|
|
||||||
|
|
||||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
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(
|
pub fn request_tool_call_permission(
|
||||||
&mut self,
|
&mut self,
|
||||||
tool_call: acp::ToolCall,
|
tool_call: acp::ToolCall,
|
||||||
|
@ -801,6 +915,25 @@ impl AcpThread {
|
||||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
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 {
|
pub fn plan(&self) -> &Plan {
|
||||||
&self.plan
|
&self.plan
|
||||||
}
|
}
|
||||||
|
@ -824,56 +957,6 @@ impl AcpThread {
|
||||||
cx.notify();
|
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<()>> {
|
pub fn authenticate(&self, cx: &mut App) -> impl use<> + Future<Output = Result<()>> {
|
||||||
self.connection.authenticate(cx)
|
self.connection.authenticate(cx)
|
||||||
}
|
}
|
||||||
|
@ -919,7 +1002,7 @@ impl AcpThread {
|
||||||
let result = this
|
let result = this
|
||||||
.update(cx, |this, cx| {
|
.update(cx, |this, cx| {
|
||||||
this.connection.prompt(
|
this.connection.prompt(
|
||||||
acp::PromptToolArguments {
|
acp::PromptArguments {
|
||||||
prompt: message,
|
prompt: message,
|
||||||
session_id: this.session_id.clone(),
|
session_id: this.session_id.clone(),
|
||||||
},
|
},
|
||||||
|
@ -1148,7 +1231,87 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[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);
|
init_test(cx);
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
|
|
@ -20,7 +20,7 @@ pub trait AgentConnection {
|
||||||
|
|
||||||
fn authenticate(&self, cx: &mut App) -> Task<Result<()>>;
|
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);
|
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 std::{cell::RefCell, error::Error, fmt, path::Path, rc::Rc};
|
||||||
use ui::App;
|
use ui::App;
|
||||||
|
|
||||||
use crate::{AcpThread, AcpThreadEvent, AgentConnection, ToolCallContent, ToolCallStatus};
|
use crate::{AcpThread, AgentConnection};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct OldAcpClientDelegate {
|
pub struct OldAcpClientDelegate {
|
||||||
|
@ -40,10 +40,10 @@ impl acp_old::Client for OldAcpClientDelegate {
|
||||||
.borrow()
|
.borrow()
|
||||||
.update(cx, |thread, cx| match params.chunk {
|
.update(cx, |thread, cx| match params.chunk {
|
||||||
acp_old::AssistantMessageChunk::Text { text } => {
|
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 } => {
|
acp_old::AssistantMessageChunk::Thought { thought } => {
|
||||||
thread.push_assistant_chunk(thought.into(), true, cx)
|
thread.push_assistant_content_block(thought.into(), true, cx)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.ok();
|
.ok();
|
||||||
|
@ -182,31 +182,23 @@ impl acp_old::Client for OldAcpClientDelegate {
|
||||||
|
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
self.thread.borrow().update(cx, |thread, cx| {
|
self.thread.borrow().update(cx, |thread, cx| {
|
||||||
let languages = thread.project.read(cx).languages().clone();
|
thread.update_tool_call(
|
||||||
|
acp::ToolCallUpdate {
|
||||||
if let Some((ix, tool_call)) = thread
|
id: acp::ToolCallId(request.tool_call_id.0.to_string().into()),
|
||||||
.tool_call_mut(&acp::ToolCallId(request.tool_call_id.0.to_string().into()))
|
fields: acp::ToolCallUpdateFields {
|
||||||
{
|
status: Some(into_new_tool_call_status(request.status)),
|
||||||
tool_call.status = ToolCallStatus::Allowed {
|
content: Some(
|
||||||
status: into_new_tool_call_status(request.status),
|
request
|
||||||
};
|
.content
|
||||||
tool_call.content = request
|
.into_iter()
|
||||||
.content
|
.map(into_new_tool_call_content)
|
||||||
.into_iter()
|
.collect::<Vec<_>>(),
|
||||||
.map(|content| {
|
),
|
||||||
ToolCallContent::from_acp(
|
..Default::default()
|
||||||
into_new_tool_call_content(content),
|
},
|
||||||
languages.clone(),
|
},
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
cx.emit(AcpThreadEvent::EntryUpdated(ix));
|
|
||||||
anyhow::Ok(())
|
|
||||||
} else {
|
|
||||||
anyhow::bail!("Tool call not found")
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
})?
|
})?
|
||||||
.context("Failed to update thread")??;
|
.context("Failed to update thread")??;
|
||||||
|
@ -285,6 +277,7 @@ fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(into_new_tool_call_location)
|
.map(into_new_tool_call_location)
|
||||||
.collect(),
|
.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 {
|
fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
|
||||||
match content {
|
match content {
|
||||||
acp_old::ToolCallContent::Markdown { markdown } => acp::ToolCallContent::ContentBlock {
|
acp_old::ToolCallContent::Markdown { markdown } => markdown.into(),
|
||||||
content: acp::ContentBlock::Text(acp::TextContent {
|
|
||||||
annotations: None,
|
|
||||||
text: markdown,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
|
acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
|
||||||
diff: into_new_diff(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
|
let chunks = params
|
||||||
.prompt
|
.prompt
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
|
@ -41,6 +41,7 @@ ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
uuid.workspace = true
|
uuid.workspace = true
|
||||||
watch.workspace = true
|
watch.workspace = true
|
||||||
|
indoc.workspace = true
|
||||||
which.workspace = true
|
which.workspace = true
|
||||||
workspace-hack.workspace = true
|
workspace-hack.workspace = true
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
mod claude;
|
mod claude;
|
||||||
|
mod codex;
|
||||||
mod gemini;
|
mod gemini;
|
||||||
|
mod mcp_server;
|
||||||
mod settings;
|
mod settings;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod e2e_tests;
|
mod e2e_tests;
|
||||||
|
|
||||||
pub use claude::*;
|
pub use claude::*;
|
||||||
|
pub use codex::*;
|
||||||
pub use gemini::*;
|
pub use gemini::*;
|
||||||
pub use settings::*;
|
pub use settings::*;
|
||||||
|
|
||||||
|
|
|
@ -44,7 +44,7 @@ impl AgentServer for ClaudeCode {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn empty_state_message(&self) -> &'static str {
|
fn empty_state_message(&self) -> &'static str {
|
||||||
""
|
"How can I help you today?"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn logo(&self) -> ui::IconName {
|
fn logo(&self) -> ui::IconName {
|
||||||
|
@ -190,7 +190,7 @@ impl AgentConnection for ClaudeAgentConnection {
|
||||||
Task::ready(Err(anyhow!("Authentication not supported")))
|
Task::ready(Err(anyhow!("Authentication not supported")))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task<Result<()>> {
|
fn prompt(&self, params: acp::PromptArguments, cx: &mut App) -> Task<Result<()>> {
|
||||||
let sessions = self.sessions.borrow();
|
let sessions = self.sessions.borrow();
|
||||||
let Some(session) = sessions.get(¶ms.session_id) else {
|
let Some(session) = sessions.get(¶ms.session_id) else {
|
||||||
return Task::ready(Err(anyhow!(
|
return Task::ready(Err(anyhow!(
|
||||||
|
@ -350,7 +350,7 @@ impl ClaudeAgentSession {
|
||||||
ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
|
ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
|
||||||
thread
|
thread
|
||||||
.update(cx, |thread, cx| {
|
.update(cx, |thread, cx| {
|
||||||
thread.push_assistant_chunk(text.into(), false, cx)
|
thread.push_assistant_content_block(text.into(), false, cx)
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
@ -387,9 +387,15 @@ impl ClaudeAgentSession {
|
||||||
thread
|
thread
|
||||||
.update(cx, |thread, cx| {
|
.update(cx, |thread, cx| {
|
||||||
thread.update_tool_call(
|
thread.update_tool_call(
|
||||||
acp::ToolCallId(tool_use_id.into()),
|
acp::ToolCallUpdate {
|
||||||
acp::ToolCallStatus::Completed,
|
id: acp::ToolCallId(tool_use_id.into()),
|
||||||
(!content.is_empty()).then(|| vec![content.into()]),
|
fields: acp::ToolCallUpdateFields {
|
||||||
|
status: Some(acp::ToolCallStatus::Completed),
|
||||||
|
content: (!content.is_empty())
|
||||||
|
.then(|| vec![content.into()]),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
},
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
@ -402,7 +408,7 @@ impl ClaudeAgentSession {
|
||||||
| ContentChunk::WebSearchToolResult => {
|
| ContentChunk::WebSearchToolResult => {
|
||||||
thread
|
thread
|
||||||
.update(cx, |thread, cx| {
|
.update(cx, |thread, cx| {
|
||||||
thread.push_assistant_chunk(
|
thread.push_assistant_content_block(
|
||||||
format!("Unsupported content: {:?}", chunk).into(),
|
format!("Unsupported content: {:?}", chunk).into(),
|
||||||
false,
|
false,
|
||||||
cx,
|
cx,
|
||||||
|
|
|
@ -311,6 +311,7 @@ impl ClaudeTool {
|
||||||
label: self.label(),
|
label: self.label(),
|
||||||
content: self.content(),
|
content: self.content(),
|
||||||
locations: self.locations(),
|
locations: self.locations(),
|
||||||
|
raw_input: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
317
crates/agent_servers/src/codex.rs
Normal file
317
crates/agent_servers/src/codex.rs
Normal file
|
@ -0,0 +1,317 @@
|
||||||
|
use agent_client_protocol as acp;
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use collections::HashMap;
|
||||||
|
use context_server::listener::McpServerTool;
|
||||||
|
use context_server::types::requests;
|
||||||
|
use context_server::{ContextServer, ContextServerCommand, ContextServerId};
|
||||||
|
use futures::channel::{mpsc, oneshot};
|
||||||
|
use project::Project;
|
||||||
|
use settings::SettingsStore;
|
||||||
|
use smol::stream::StreamExt as _;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::{path::Path, sync::Arc};
|
||||||
|
use util::ResultExt;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
|
||||||
|
|
||||||
|
use crate::mcp_server::ZedMcpServer;
|
||||||
|
use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings, mcp_server};
|
||||||
|
use acp_thread::{AcpThread, AgentConnection};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Codex;
|
||||||
|
|
||||||
|
impl AgentServer for Codex {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Codex"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_state_headline(&self) -> &'static str {
|
||||||
|
"Welcome to Codex"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn empty_state_message(&self) -> &'static str {
|
||||||
|
"What can I help with?"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logo(&self) -> ui::IconName {
|
||||||
|
ui::IconName::AiOpenAi
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connect(
|
||||||
|
&self,
|
||||||
|
_root_dir: &Path,
|
||||||
|
project: &Entity<Project>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||||
|
let project = project.clone();
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||||
|
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let Some(command) =
|
||||||
|
AgentServerCommand::resolve("codex", &["mcp"], settings, &project, cx).await
|
||||||
|
else {
|
||||||
|
anyhow::bail!("Failed to find codex binary");
|
||||||
|
};
|
||||||
|
|
||||||
|
let client: Arc<ContextServer> = ContextServer::stdio(
|
||||||
|
ContextServerId("codex-mcp-server".into()),
|
||||||
|
ContextServerCommand {
|
||||||
|
path: command.path,
|
||||||
|
args: command.args,
|
||||||
|
env: command.env,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.into();
|
||||||
|
ContextServer::start(client.clone(), cx).await?;
|
||||||
|
|
||||||
|
let (notification_tx, mut notification_rx) = mpsc::unbounded();
|
||||||
|
client
|
||||||
|
.client()
|
||||||
|
.context("Failed to subscribe")?
|
||||||
|
.on_notification(acp::SESSION_UPDATE_METHOD_NAME, {
|
||||||
|
move |notification, _cx| {
|
||||||
|
let notification_tx = notification_tx.clone();
|
||||||
|
log::trace!(
|
||||||
|
"ACP Notification: {}",
|
||||||
|
serde_json::to_string_pretty(¬ification).unwrap()
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(notification) =
|
||||||
|
serde_json::from_value::<acp::SessionNotification>(notification)
|
||||||
|
.log_err()
|
||||||
|
{
|
||||||
|
notification_tx.unbounded_send(notification).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let sessions = Rc::new(RefCell::new(HashMap::default()));
|
||||||
|
|
||||||
|
let notification_handler_task = cx.spawn({
|
||||||
|
let sessions = sessions.clone();
|
||||||
|
async move |cx| {
|
||||||
|
while let Some(notification) = notification_rx.next().await {
|
||||||
|
CodexConnection::handle_session_notification(
|
||||||
|
notification,
|
||||||
|
sessions.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let connection = CodexConnection {
|
||||||
|
client,
|
||||||
|
sessions,
|
||||||
|
_notification_handler_task: notification_handler_task,
|
||||||
|
};
|
||||||
|
Ok(Rc::new(connection) as _)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CodexConnection {
|
||||||
|
client: Arc<context_server::ContextServer>,
|
||||||
|
sessions: Rc<RefCell<HashMap<acp::SessionId, CodexSession>>>,
|
||||||
|
_notification_handler_task: Task<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CodexSession {
|
||||||
|
thread: WeakEntity<AcpThread>,
|
||||||
|
cancel_tx: Option<oneshot::Sender<()>>,
|
||||||
|
_mcp_server: ZedMcpServer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AgentConnection for CodexConnection {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"Codex"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_thread(
|
||||||
|
self: Rc<Self>,
|
||||||
|
project: Entity<Project>,
|
||||||
|
cwd: &Path,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
) -> Task<Result<Entity<AcpThread>>> {
|
||||||
|
let client = self.client.client();
|
||||||
|
let sessions = self.sessions.clone();
|
||||||
|
let cwd = cwd.to_path_buf();
|
||||||
|
cx.spawn(async move |cx| {
|
||||||
|
let client = client.context("MCP server is not initialized yet")?;
|
||||||
|
let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
|
||||||
|
|
||||||
|
let mcp_server = ZedMcpServer::new(thread_rx, cx).await?;
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.request::<requests::CallTool>(context_server::types::CallToolParams {
|
||||||
|
name: acp::NEW_SESSION_TOOL_NAME.into(),
|
||||||
|
arguments: Some(serde_json::to_value(acp::NewSessionArguments {
|
||||||
|
mcp_servers: [(
|
||||||
|
mcp_server::SERVER_NAME.to_string(),
|
||||||
|
mcp_server.server_config()?,
|
||||||
|
)]
|
||||||
|
.into(),
|
||||||
|
client_tools: acp::ClientTools {
|
||||||
|
request_permission: Some(acp::McpToolId {
|
||||||
|
mcp_server: mcp_server::SERVER_NAME.into(),
|
||||||
|
tool_name: mcp_server::RequestPermissionTool::NAME.into(),
|
||||||
|
}),
|
||||||
|
read_text_file: Some(acp::McpToolId {
|
||||||
|
mcp_server: mcp_server::SERVER_NAME.into(),
|
||||||
|
tool_name: mcp_server::ReadTextFileTool::NAME.into(),
|
||||||
|
}),
|
||||||
|
write_text_file: Some(acp::McpToolId {
|
||||||
|
mcp_server: mcp_server::SERVER_NAME.into(),
|
||||||
|
tool_name: mcp_server::WriteTextFileTool::NAME.into(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
cwd,
|
||||||
|
})?),
|
||||||
|
meta: None,
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if response.is_error.unwrap_or_default() {
|
||||||
|
return Err(anyhow!(response.text_contents()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = serde_json::from_value::<acp::NewSessionOutput>(
|
||||||
|
response.structured_content.context("Empty response")?,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let thread =
|
||||||
|
cx.new(|cx| AcpThread::new(self.clone(), project, result.session_id.clone(), cx))?;
|
||||||
|
|
||||||
|
thread_tx.send(thread.downgrade())?;
|
||||||
|
|
||||||
|
let session = CodexSession {
|
||||||
|
thread: thread.downgrade(),
|
||||||
|
cancel_tx: None,
|
||||||
|
_mcp_server: mcp_server,
|
||||||
|
};
|
||||||
|
sessions.borrow_mut().insert(result.session_id, session);
|
||||||
|
|
||||||
|
Ok(thread)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn authenticate(&self, _cx: &mut App) -> Task<Result<()>> {
|
||||||
|
Task::ready(Err(anyhow!("Authentication not supported")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(
|
||||||
|
&self,
|
||||||
|
params: agent_client_protocol::PromptArguments,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
let client = self.client.client();
|
||||||
|
let sessions = self.sessions.clone();
|
||||||
|
|
||||||
|
cx.foreground_executor().spawn(async move {
|
||||||
|
let client = client.context("MCP server is not initialized yet")?;
|
||||||
|
|
||||||
|
let (new_cancel_tx, cancel_rx) = oneshot::channel();
|
||||||
|
{
|
||||||
|
let mut sessions = sessions.borrow_mut();
|
||||||
|
let session = sessions
|
||||||
|
.get_mut(¶ms.session_id)
|
||||||
|
.context("Session not found")?;
|
||||||
|
session.cancel_tx.replace(new_cancel_tx);
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = client
|
||||||
|
.request_with::<requests::CallTool>(
|
||||||
|
context_server::types::CallToolParams {
|
||||||
|
name: acp::PROMPT_TOOL_NAME.into(),
|
||||||
|
arguments: Some(serde_json::to_value(params)?),
|
||||||
|
meta: None,
|
||||||
|
},
|
||||||
|
Some(cancel_rx),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if let Err(err) = &result
|
||||||
|
&& err.is::<context_server::client::RequestCanceled>()
|
||||||
|
{
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = result?;
|
||||||
|
|
||||||
|
if response.is_error.unwrap_or_default() {
|
||||||
|
return Err(anyhow!(response.text_contents()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel(&self, session_id: &agent_client_protocol::SessionId, _cx: &mut App) {
|
||||||
|
let mut sessions = self.sessions.borrow_mut();
|
||||||
|
|
||||||
|
if let Some(cancel_tx) = sessions
|
||||||
|
.get_mut(session_id)
|
||||||
|
.and_then(|session| session.cancel_tx.take())
|
||||||
|
{
|
||||||
|
cancel_tx.send(()).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CodexConnection {
|
||||||
|
pub fn handle_session_notification(
|
||||||
|
notification: acp::SessionNotification,
|
||||||
|
threads: Rc<RefCell<HashMap<acp::SessionId, CodexSession>>>,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
) {
|
||||||
|
let threads = threads.borrow();
|
||||||
|
let Some(thread) = threads
|
||||||
|
.get(¬ification.session_id)
|
||||||
|
.and_then(|session| session.thread.upgrade())
|
||||||
|
else {
|
||||||
|
log::error!(
|
||||||
|
"Thread not found for session ID: {}",
|
||||||
|
notification.session_id
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
thread
|
||||||
|
.update(cx, |thread, cx| {
|
||||||
|
thread.handle_session_update(notification.update, cx)
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for CodexConnection {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.client.stop().log_err();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub(crate) mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::AgentServerCommand;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
crate::common_e2e_tests!(Codex);
|
||||||
|
|
||||||
|
pub fn local_command() -> AgentServerCommand {
|
||||||
|
let cli_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../../../codex/codex-rs/target/debug/codex");
|
||||||
|
|
||||||
|
AgentServerCommand {
|
||||||
|
path: cli_path,
|
||||||
|
args: vec!["mcp".into()],
|
||||||
|
env: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -351,6 +351,9 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||||
gemini: Some(AgentServerSettings {
|
gemini: Some(AgentServerSettings {
|
||||||
command: crate::gemini::tests::local_command(),
|
command: crate::gemini::tests::local_command(),
|
||||||
}),
|
}),
|
||||||
|
codex: Some(AgentServerSettings {
|
||||||
|
command: crate::codex::tests::local_command(),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
|
|
201
crates/agent_servers/src/mcp_server.rs
Normal file
201
crates/agent_servers/src/mcp_server.rs
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
use acp_thread::AcpThread;
|
||||||
|
use agent_client_protocol as acp;
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use context_server::listener::{McpServerTool, ToolResponse};
|
||||||
|
use context_server::types::{
|
||||||
|
Implementation, InitializeParams, InitializeResponse, ProtocolVersion, ServerCapabilities,
|
||||||
|
ToolsCapabilities, requests,
|
||||||
|
};
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use gpui::{App, AsyncApp, Task, WeakEntity};
|
||||||
|
use indoc::indoc;
|
||||||
|
|
||||||
|
pub struct ZedMcpServer {
|
||||||
|
server: context_server::listener::McpServer,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const SERVER_NAME: &str = "zed";
|
||||||
|
|
||||||
|
impl ZedMcpServer {
|
||||||
|
pub async fn new(
|
||||||
|
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||||
|
cx: &AsyncApp,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
|
||||||
|
mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
|
||||||
|
|
||||||
|
mcp_server.add_tool(RequestPermissionTool {
|
||||||
|
thread_rx: thread_rx.clone(),
|
||||||
|
});
|
||||||
|
mcp_server.add_tool(ReadTextFileTool {
|
||||||
|
thread_rx: thread_rx.clone(),
|
||||||
|
});
|
||||||
|
mcp_server.add_tool(WriteTextFileTool {
|
||||||
|
thread_rx: thread_rx.clone(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self { server: mcp_server })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn server_config(&self) -> Result<acp::McpServerConfig> {
|
||||||
|
let zed_path = std::env::current_exe()
|
||||||
|
.context("finding current executable path for use in mcp_server")?;
|
||||||
|
|
||||||
|
Ok(acp::McpServerConfig {
|
||||||
|
command: zed_path,
|
||||||
|
args: vec![
|
||||||
|
"--nc".into(),
|
||||||
|
self.server.socket_path().display().to_string(),
|
||||||
|
],
|
||||||
|
env: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_initialize(_: InitializeParams, cx: &App) -> Task<Result<InitializeResponse>> {
|
||||||
|
cx.foreground_executor().spawn(async move {
|
||||||
|
Ok(InitializeResponse {
|
||||||
|
protocol_version: ProtocolVersion("2025-06-18".into()),
|
||||||
|
capabilities: ServerCapabilities {
|
||||||
|
experimental: None,
|
||||||
|
logging: None,
|
||||||
|
completions: None,
|
||||||
|
prompts: None,
|
||||||
|
resources: None,
|
||||||
|
tools: Some(ToolsCapabilities {
|
||||||
|
list_changed: Some(false),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
server_info: Implementation {
|
||||||
|
name: SERVER_NAME.into(),
|
||||||
|
version: "0.1.0".into(),
|
||||||
|
},
|
||||||
|
meta: None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tools
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct RequestPermissionTool {
|
||||||
|
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl McpServerTool for RequestPermissionTool {
|
||||||
|
type Input = acp::RequestPermissionArguments;
|
||||||
|
type Output = acp::RequestPermissionOutput;
|
||||||
|
|
||||||
|
const NAME: &'static str = "Confirmation";
|
||||||
|
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
indoc! {"
|
||||||
|
Request permission for tool calls.
|
||||||
|
|
||||||
|
This tool is meant to be called programmatically by the agent loop, not the LLM.
|
||||||
|
"}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(
|
||||||
|
&self,
|
||||||
|
input: Self::Input,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
) -> Result<ToolResponse<Self::Output>> {
|
||||||
|
let mut thread_rx = self.thread_rx.clone();
|
||||||
|
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
||||||
|
anyhow::bail!("Thread closed");
|
||||||
|
};
|
||||||
|
|
||||||
|
let result = thread
|
||||||
|
.update(cx, |thread, cx| {
|
||||||
|
thread.request_tool_call_permission(input.tool_call, input.options, cx)
|
||||||
|
})?
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let outcome = match result {
|
||||||
|
Ok(option_id) => acp::RequestPermissionOutcome::Selected { option_id },
|
||||||
|
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Canceled,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(ToolResponse {
|
||||||
|
content: vec![],
|
||||||
|
structured_content: acp::RequestPermissionOutput { outcome },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct ReadTextFileTool {
|
||||||
|
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl McpServerTool for ReadTextFileTool {
|
||||||
|
type Input = acp::ReadTextFileArguments;
|
||||||
|
type Output = acp::ReadTextFileOutput;
|
||||||
|
|
||||||
|
const NAME: &'static str = "Read";
|
||||||
|
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Reads the content of the given file in the project including unsaved changes."
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(
|
||||||
|
&self,
|
||||||
|
input: Self::Input,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
) -> Result<ToolResponse<Self::Output>> {
|
||||||
|
let mut thread_rx = self.thread_rx.clone();
|
||||||
|
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
||||||
|
anyhow::bail!("Thread closed");
|
||||||
|
};
|
||||||
|
|
||||||
|
let content = thread
|
||||||
|
.update(cx, |thread, cx| {
|
||||||
|
thread.read_text_file(input.path, input.line, input.limit, false, cx)
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(ToolResponse {
|
||||||
|
content: vec![],
|
||||||
|
structured_content: acp::ReadTextFileOutput { content },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct WriteTextFileTool {
|
||||||
|
thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl McpServerTool for WriteTextFileTool {
|
||||||
|
type Input = acp::WriteTextFileArguments;
|
||||||
|
type Output = ();
|
||||||
|
|
||||||
|
const NAME: &'static str = "Write";
|
||||||
|
|
||||||
|
fn description(&self) -> &'static str {
|
||||||
|
"Write to a file replacing its contents"
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run(
|
||||||
|
&self,
|
||||||
|
input: Self::Input,
|
||||||
|
cx: &mut AsyncApp,
|
||||||
|
) -> Result<ToolResponse<Self::Output>> {
|
||||||
|
let mut thread_rx = self.thread_rx.clone();
|
||||||
|
let Some(thread) = thread_rx.recv().await?.upgrade() else {
|
||||||
|
anyhow::bail!("Thread closed");
|
||||||
|
};
|
||||||
|
|
||||||
|
thread
|
||||||
|
.update(cx, |thread, cx| {
|
||||||
|
thread.write_text_file(input.path, input.content, cx)
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(ToolResponse {
|
||||||
|
content: vec![],
|
||||||
|
structured_content: (),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,6 +13,7 @@ pub fn init(cx: &mut App) {
|
||||||
pub struct AllAgentServersSettings {
|
pub struct AllAgentServersSettings {
|
||||||
pub gemini: Option<AgentServerSettings>,
|
pub gemini: Option<AgentServerSettings>,
|
||||||
pub claude: Option<AgentServerSettings>,
|
pub claude: Option<AgentServerSettings>,
|
||||||
|
pub codex: Option<AgentServerSettings>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
|
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
|
||||||
|
@ -29,13 +30,21 @@ impl settings::Settings for AllAgentServersSettings {
|
||||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||||
let mut settings = AllAgentServersSettings::default();
|
let mut settings = AllAgentServersSettings::default();
|
||||||
|
|
||||||
for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() {
|
for AllAgentServersSettings {
|
||||||
|
gemini,
|
||||||
|
claude,
|
||||||
|
codex,
|
||||||
|
} in sources.defaults_and_customizations()
|
||||||
|
{
|
||||||
if gemini.is_some() {
|
if gemini.is_some() {
|
||||||
settings.gemini = gemini.clone();
|
settings.gemini = gemini.clone();
|
||||||
}
|
}
|
||||||
if claude.is_some() {
|
if claude.is_some() {
|
||||||
settings.claude = claude.clone();
|
settings.claude = claude.clone();
|
||||||
}
|
}
|
||||||
|
if codex.is_some() {
|
||||||
|
settings.codex = codex.clone();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(settings)
|
Ok(settings)
|
||||||
|
|
|
@ -872,7 +872,10 @@ impl AcpThreadView {
|
||||||
let header_id = SharedString::from(format!("tool-call-header-{}", entry_ix));
|
let header_id = SharedString::from(format!("tool-call-header-{}", entry_ix));
|
||||||
|
|
||||||
let status_icon = match &tool_call.status {
|
let status_icon = match &tool_call.status {
|
||||||
ToolCallStatus::WaitingForConfirmation { .. } => None,
|
ToolCallStatus::Allowed {
|
||||||
|
status: acp::ToolCallStatus::Pending,
|
||||||
|
}
|
||||||
|
| ToolCallStatus::WaitingForConfirmation { .. } => None,
|
||||||
ToolCallStatus::Allowed {
|
ToolCallStatus::Allowed {
|
||||||
status: acp::ToolCallStatus::InProgress,
|
status: acp::ToolCallStatus::InProgress,
|
||||||
..
|
..
|
||||||
|
@ -957,6 +960,8 @@ impl AcpThreadView {
|
||||||
Icon::new(match tool_call.kind {
|
Icon::new(match tool_call.kind {
|
||||||
acp::ToolKind::Read => IconName::ToolRead,
|
acp::ToolKind::Read => IconName::ToolRead,
|
||||||
acp::ToolKind::Edit => IconName::ToolPencil,
|
acp::ToolKind::Edit => IconName::ToolPencil,
|
||||||
|
acp::ToolKind::Delete => IconName::ToolDeleteFile,
|
||||||
|
acp::ToolKind::Move => IconName::ArrowRightLeft,
|
||||||
acp::ToolKind::Search => IconName::ToolSearch,
|
acp::ToolKind::Search => IconName::ToolSearch,
|
||||||
acp::ToolKind::Execute => IconName::ToolTerminal,
|
acp::ToolKind::Execute => IconName::ToolTerminal,
|
||||||
acp::ToolKind::Think => IconName::ToolBulb,
|
acp::ToolKind::Think => IconName::ToolBulb,
|
||||||
|
@ -1068,6 +1073,7 @@ impl AcpThreadView {
|
||||||
options,
|
options,
|
||||||
entry_ix,
|
entry_ix,
|
||||||
tool_call.id.clone(),
|
tool_call.id.clone(),
|
||||||
|
tool_call.content.is_empty(),
|
||||||
cx,
|
cx,
|
||||||
)),
|
)),
|
||||||
ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
|
ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
|
||||||
|
@ -1126,6 +1132,7 @@ impl AcpThreadView {
|
||||||
options: &[acp::PermissionOption],
|
options: &[acp::PermissionOption],
|
||||||
entry_ix: usize,
|
entry_ix: usize,
|
||||||
tool_call_id: acp::ToolCallId,
|
tool_call_id: acp::ToolCallId,
|
||||||
|
empty_content: bool,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> Div {
|
) -> Div {
|
||||||
h_flex()
|
h_flex()
|
||||||
|
@ -1133,8 +1140,10 @@ impl AcpThreadView {
|
||||||
.px_1p5()
|
.px_1p5()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.justify_end()
|
.justify_end()
|
||||||
.border_t_1()
|
.when(!empty_content, |this| {
|
||||||
.border_color(self.tool_card_border_color(cx))
|
this.border_t_1()
|
||||||
|
.border_color(self.tool_card_border_color(cx))
|
||||||
|
})
|
||||||
.children(options.iter().map(|option| {
|
.children(options.iter().map(|option| {
|
||||||
let option_id = SharedString::from(option.id.0.clone());
|
let option_id = SharedString::from(option.id.0.clone());
|
||||||
Button::new((option_id, entry_ix), option.label.clone())
|
Button::new((option_id, entry_ix), option.label.clone())
|
||||||
|
|
|
@ -1991,6 +1991,20 @@ impl AgentPanel {
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
.item(
|
||||||
|
ContextMenuEntry::new("New Codex Thread")
|
||||||
|
.icon(IconName::AiOpenAi)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.handler(move |window, cx| {
|
||||||
|
window.dispatch_action(
|
||||||
|
NewExternalAgentThread {
|
||||||
|
agent: Some(crate::ExternalAgent::Codex),
|
||||||
|
}
|
||||||
|
.boxed_clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
)
|
||||||
});
|
});
|
||||||
menu
|
menu
|
||||||
}))
|
}))
|
||||||
|
@ -2652,6 +2666,25 @@ impl AgentPanel {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
NewThreadButton::new(
|
||||||
|
"new-codex-thread-btn",
|
||||||
|
"New Codex Thread",
|
||||||
|
IconName::AiOpenAi,
|
||||||
|
)
|
||||||
|
.on_click(
|
||||||
|
|window, cx| {
|
||||||
|
window.dispatch_action(
|
||||||
|
Box::new(NewExternalAgentThread {
|
||||||
|
agent: Some(
|
||||||
|
crate::ExternalAgent::Codex,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
|
|
|
@ -150,6 +150,7 @@ enum ExternalAgent {
|
||||||
#[default]
|
#[default]
|
||||||
Gemini,
|
Gemini,
|
||||||
ClaudeCode,
|
ClaudeCode,
|
||||||
|
Codex,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExternalAgent {
|
impl ExternalAgent {
|
||||||
|
@ -157,6 +158,7 @@ impl ExternalAgent {
|
||||||
match self {
|
match self {
|
||||||
ExternalAgent::Gemini => Rc::new(agent_servers::Gemini),
|
ExternalAgent::Gemini => Rc::new(agent_servers::Gemini),
|
||||||
ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
|
ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
|
||||||
|
ExternalAgent::Codex => Rc::new(agent_servers::Codex),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -330,23 +330,16 @@ impl Client {
|
||||||
method: &str,
|
method: &str,
|
||||||
params: impl Serialize,
|
params: impl Serialize,
|
||||||
) -> Result<T> {
|
) -> Result<T> {
|
||||||
self.request_impl(method, params, None).await
|
self.request_with(method, params, None, Some(REQUEST_TIMEOUT))
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cancellable_request<T: DeserializeOwned>(
|
pub async fn request_with<T: DeserializeOwned>(
|
||||||
&self,
|
|
||||||
method: &str,
|
|
||||||
params: impl Serialize,
|
|
||||||
cancel_rx: oneshot::Receiver<()>,
|
|
||||||
) -> Result<T> {
|
|
||||||
self.request_impl(method, params, Some(cancel_rx)).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn request_impl<T: DeserializeOwned>(
|
|
||||||
&self,
|
&self,
|
||||||
method: &str,
|
method: &str,
|
||||||
params: impl Serialize,
|
params: impl Serialize,
|
||||||
cancel_rx: Option<oneshot::Receiver<()>>,
|
cancel_rx: Option<oneshot::Receiver<()>>,
|
||||||
|
timeout: Option<Duration>,
|
||||||
) -> Result<T> {
|
) -> Result<T> {
|
||||||
let id = self.next_id.fetch_add(1, SeqCst);
|
let id = self.next_id.fetch_add(1, SeqCst);
|
||||||
let request = serde_json::to_string(&Request {
|
let request = serde_json::to_string(&Request {
|
||||||
|
@ -382,7 +375,13 @@ impl Client {
|
||||||
handle_response?;
|
handle_response?;
|
||||||
send?;
|
send?;
|
||||||
|
|
||||||
let mut timeout = executor.timer(REQUEST_TIMEOUT).fuse();
|
let mut timeout_fut = pin!(
|
||||||
|
match timeout {
|
||||||
|
Some(timeout) => future::Either::Left(executor.timer(timeout)),
|
||||||
|
None => future::Either::Right(future::pending()),
|
||||||
|
}
|
||||||
|
.fuse()
|
||||||
|
);
|
||||||
let mut cancel_fut = pin!(
|
let mut cancel_fut = pin!(
|
||||||
match cancel_rx {
|
match cancel_rx {
|
||||||
Some(rx) => future::Either::Left(async {
|
Some(rx) => future::Either::Left(async {
|
||||||
|
@ -419,10 +418,10 @@ impl Client {
|
||||||
reason: None
|
reason: None
|
||||||
})
|
})
|
||||||
).log_err();
|
).log_err();
|
||||||
anyhow::bail!("Request cancelled")
|
anyhow::bail!(RequestCanceled)
|
||||||
}
|
}
|
||||||
_ = timeout => {
|
_ = timeout_fut => {
|
||||||
log::error!("cancelled csp request task for {method:?} id {id} which took over {:?}", REQUEST_TIMEOUT);
|
log::error!("cancelled csp request task for {method:?} id {id} which took over {:?}", timeout.unwrap());
|
||||||
anyhow::bail!("Context server request timeout");
|
anyhow::bail!("Context server request timeout");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -452,6 +451,17 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RequestCanceled;
|
||||||
|
|
||||||
|
impl std::error::Error for RequestCanceled {}
|
||||||
|
|
||||||
|
impl std::fmt::Display for RequestCanceled {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str("Context server request was canceled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl fmt::Display for ContextServerId {
|
impl fmt::Display for ContextServerId {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
self.0.fmt(f)
|
self.0.fmt(f)
|
||||||
|
|
|
@ -419,7 +419,7 @@ pub struct ToolResponse<T> {
|
||||||
pub structured_content: T,
|
pub structured_content: T,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
struct RawRequest {
|
struct RawRequest {
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
id: Option<RequestId>,
|
id: Option<RequestId>,
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
//! read/write messages and the types from types.rs for serialization/deserialization
|
//! read/write messages and the types from types.rs for serialization/deserialization
|
||||||
//! of messages.
|
//! of messages.
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use gpui::AsyncApp;
|
use gpui::AsyncApp;
|
||||||
|
@ -98,13 +100,14 @@ impl InitializedContextServerProtocol {
|
||||||
self.inner.request(T::METHOD, params).await
|
self.inner.request(T::METHOD, params).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cancellable_request<T: Request>(
|
pub async fn request_with<T: Request>(
|
||||||
&self,
|
&self,
|
||||||
params: T::Params,
|
params: T::Params,
|
||||||
cancel_rx: oneshot::Receiver<()>,
|
cancel_rx: Option<oneshot::Receiver<()>>,
|
||||||
|
timeout: Option<Duration>,
|
||||||
) -> Result<T::Response> {
|
) -> Result<T::Response> {
|
||||||
self.inner
|
self.inner
|
||||||
.cancellable_request(T::METHOD, params, cancel_rx)
|
.request_with(T::METHOD, params, cancel_rx, timeout)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -626,6 +626,7 @@ pub enum ClientNotification {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CancelledParams {
|
pub struct CancelledParams {
|
||||||
pub request_id: RequestId,
|
pub request_id: RequestId,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
@ -685,6 +686,18 @@ pub struct CallToolResponse {
|
||||||
pub structured_content: Option<serde_json::Value>,
|
pub structured_content: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl CallToolResponse {
|
||||||
|
pub fn text_contents(&self) -> String {
|
||||||
|
let mut text = String::new();
|
||||||
|
for chunk in &self.content {
|
||||||
|
if let ToolResponseContent::Text { text: chunk } = chunk {
|
||||||
|
text.push_str(&chunk)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum ToolResponseContent {
|
pub enum ToolResponseContent {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue