Lay the groundwork to support history in agent2 (#36483)
This pull request introduces title generation and history replaying. We still need to wire up the rest of the history but this gets us very close. I extracted a lot of this code from `agent2-history` because that branch was starting to get long-lived and there were lots of changes since we started. Release Notes: - N/A
This commit is contained in:
parent
c4083b9b63
commit
6c255c1973
19 changed files with 929 additions and 328 deletions
|
@ -1,10 +1,11 @@
|
|||
use crate::{
|
||||
AgentResponseEvent, ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool,
|
||||
DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool,
|
||||
MovePathTool, NowTool, OpenTool, ReadFileTool, TerminalTool, ThinkingTool, Thread,
|
||||
ToolCallAuthorization, UserMessageContent, WebSearchTool, templates::Templates,
|
||||
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DeletePathTool, DiagnosticsTool,
|
||||
EditFileTool, FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MovePathTool, NowTool,
|
||||
OpenTool, ReadFileTool, TerminalTool, ThinkingTool, Thread, ThreadEvent, ToolCallAuthorization,
|
||||
UserMessageContent, WebSearchTool, templates::Templates,
|
||||
};
|
||||
use acp_thread::AgentModelSelector;
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol as acp;
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
|
@ -427,18 +428,19 @@ impl NativeAgent {
|
|||
) {
|
||||
self.models.refresh_list(cx);
|
||||
|
||||
let default_model = LanguageModelRegistry::read_global(cx)
|
||||
.default_model()
|
||||
.map(|m| m.model.clone());
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
let default_model = registry.default_model().map(|m| m.model.clone());
|
||||
let summarization_model = registry.thread_summary_model().map(|m| m.model.clone());
|
||||
|
||||
for session in self.sessions.values_mut() {
|
||||
session.thread.update(cx, |thread, cx| {
|
||||
if thread.model().is_none()
|
||||
&& let Some(model) = default_model.clone()
|
||||
{
|
||||
thread.set_model(model);
|
||||
thread.set_model(model, cx);
|
||||
cx.notify();
|
||||
}
|
||||
thread.set_summarization_model(summarization_model.clone(), cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -462,10 +464,7 @@ impl NativeAgentConnection {
|
|||
session_id: acp::SessionId,
|
||||
cx: &mut App,
|
||||
f: impl 'static
|
||||
+ FnOnce(
|
||||
Entity<Thread>,
|
||||
&mut App,
|
||||
) -> Result<mpsc::UnboundedReceiver<Result<AgentResponseEvent>>>,
|
||||
+ FnOnce(Entity<Thread>, &mut App) -> Result<mpsc::UnboundedReceiver<Result<ThreadEvent>>>,
|
||||
) -> Task<Result<acp::PromptResponse>> {
|
||||
let Some((thread, acp_thread)) = self.0.update(cx, |agent, _cx| {
|
||||
agent
|
||||
|
@ -489,7 +488,18 @@ impl NativeAgentConnection {
|
|||
log::trace!("Received completion event: {:?}", event);
|
||||
|
||||
match event {
|
||||
AgentResponseEvent::Text(text) => {
|
||||
ThreadEvent::UserMessage(message) => {
|
||||
acp_thread.update(cx, |thread, cx| {
|
||||
for content in message.content {
|
||||
thread.push_user_content_block(
|
||||
Some(message.id.clone()),
|
||||
content.into(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})?;
|
||||
}
|
||||
ThreadEvent::AgentText(text) => {
|
||||
acp_thread.update(cx, |thread, cx| {
|
||||
thread.push_assistant_content_block(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
|
@ -501,7 +511,7 @@ impl NativeAgentConnection {
|
|||
)
|
||||
})?;
|
||||
}
|
||||
AgentResponseEvent::Thinking(text) => {
|
||||
ThreadEvent::AgentThinking(text) => {
|
||||
acp_thread.update(cx, |thread, cx| {
|
||||
thread.push_assistant_content_block(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
|
@ -513,7 +523,7 @@ impl NativeAgentConnection {
|
|||
)
|
||||
})?;
|
||||
}
|
||||
AgentResponseEvent::ToolCallAuthorization(ToolCallAuthorization {
|
||||
ThreadEvent::ToolCallAuthorization(ToolCallAuthorization {
|
||||
tool_call,
|
||||
options,
|
||||
response,
|
||||
|
@ -536,22 +546,26 @@ impl NativeAgentConnection {
|
|||
})
|
||||
.detach();
|
||||
}
|
||||
AgentResponseEvent::ToolCall(tool_call) => {
|
||||
ThreadEvent::ToolCall(tool_call) => {
|
||||
acp_thread.update(cx, |thread, cx| {
|
||||
thread.upsert_tool_call(tool_call, cx)
|
||||
})??;
|
||||
}
|
||||
AgentResponseEvent::ToolCallUpdate(update) => {
|
||||
ThreadEvent::ToolCallUpdate(update) => {
|
||||
acp_thread.update(cx, |thread, cx| {
|
||||
thread.update_tool_call(update, cx)
|
||||
})??;
|
||||
}
|
||||
AgentResponseEvent::Retry(status) => {
|
||||
ThreadEvent::TitleUpdate(title) => {
|
||||
acp_thread
|
||||
.update(cx, |thread, cx| thread.update_title(title, cx))??;
|
||||
}
|
||||
ThreadEvent::Retry(status) => {
|
||||
acp_thread.update(cx, |thread, cx| {
|
||||
thread.update_retry_status(status, cx)
|
||||
})?;
|
||||
}
|
||||
AgentResponseEvent::Stop(stop_reason) => {
|
||||
ThreadEvent::Stop(stop_reason) => {
|
||||
log::debug!("Assistant message complete: {:?}", stop_reason);
|
||||
return Ok(acp::PromptResponse { stop_reason });
|
||||
}
|
||||
|
@ -604,8 +618,8 @@ impl AgentModelSelector for NativeAgentConnection {
|
|||
return Task::ready(Err(anyhow!("Invalid model ID {}", model_id)));
|
||||
};
|
||||
|
||||
thread.update(cx, |thread, _cx| {
|
||||
thread.set_model(model.clone());
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_model(model.clone(), cx);
|
||||
});
|
||||
|
||||
update_settings_file::<AgentSettings>(
|
||||
|
@ -665,30 +679,14 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
cx.spawn(async move |cx| {
|
||||
log::debug!("Starting thread creation in async context");
|
||||
|
||||
// Generate session ID
|
||||
let session_id = acp::SessionId(uuid::Uuid::new_v4().to_string().into());
|
||||
log::info!("Created session with ID: {}", session_id);
|
||||
|
||||
// Create AcpThread
|
||||
let acp_thread = cx.update(|cx| {
|
||||
cx.new(|cx| {
|
||||
acp_thread::AcpThread::new(
|
||||
"agent2",
|
||||
self.clone(),
|
||||
project.clone(),
|
||||
session_id.clone(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?;
|
||||
let action_log = cx.update(|cx| acp_thread.read(cx).action_log().clone())?;
|
||||
|
||||
let action_log = cx.new(|_cx| ActionLog::new(project.clone()))?;
|
||||
// Create Thread
|
||||
let thread = agent.update(
|
||||
cx,
|
||||
|agent, cx: &mut gpui::Context<NativeAgent>| -> Result<_> {
|
||||
// Fetch default model from registry settings
|
||||
let registry = LanguageModelRegistry::read_global(cx);
|
||||
let language_registry = project.read(cx).languages().clone();
|
||||
|
||||
// Log available models for debugging
|
||||
let available_count = registry.available_models(cx).count();
|
||||
|
@ -699,6 +697,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
.models
|
||||
.model_from_id(&LanguageModels::model_id(&default_model.model))
|
||||
});
|
||||
let summarization_model = registry.thread_summary_model().map(|c| c.model);
|
||||
|
||||
let thread = cx.new(|cx| {
|
||||
let mut thread = Thread::new(
|
||||
|
@ -708,13 +707,14 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
action_log.clone(),
|
||||
agent.templates.clone(),
|
||||
default_model,
|
||||
summarization_model,
|
||||
cx,
|
||||
);
|
||||
thread.add_tool(CopyPathTool::new(project.clone()));
|
||||
thread.add_tool(CreateDirectoryTool::new(project.clone()));
|
||||
thread.add_tool(DeletePathTool::new(project.clone(), action_log.clone()));
|
||||
thread.add_tool(DiagnosticsTool::new(project.clone()));
|
||||
thread.add_tool(EditFileTool::new(cx.entity()));
|
||||
thread.add_tool(EditFileTool::new(cx.weak_entity(), language_registry));
|
||||
thread.add_tool(FetchTool::new(project.read(cx).client().http_client()));
|
||||
thread.add_tool(FindPathTool::new(project.clone()));
|
||||
thread.add_tool(GrepTool::new(project.clone()));
|
||||
|
@ -722,7 +722,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
thread.add_tool(MovePathTool::new(project.clone()));
|
||||
thread.add_tool(NowTool);
|
||||
thread.add_tool(OpenTool::new(project.clone()));
|
||||
thread.add_tool(ReadFileTool::new(project.clone(), action_log));
|
||||
thread.add_tool(ReadFileTool::new(project.clone(), action_log.clone()));
|
||||
thread.add_tool(TerminalTool::new(project.clone(), cx));
|
||||
thread.add_tool(ThinkingTool);
|
||||
thread.add_tool(WebSearchTool); // TODO: Enable this only if it's a zed model.
|
||||
|
@ -733,6 +733,21 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
},
|
||||
)??;
|
||||
|
||||
let session_id = thread.read_with(cx, |thread, _| thread.id().clone())?;
|
||||
log::info!("Created session with ID: {}", session_id);
|
||||
// Create AcpThread
|
||||
let acp_thread = cx.update(|cx| {
|
||||
cx.new(|_cx| {
|
||||
acp_thread::AcpThread::new(
|
||||
"agent2",
|
||||
self.clone(),
|
||||
project.clone(),
|
||||
action_log.clone(),
|
||||
session_id.clone(),
|
||||
)
|
||||
})
|
||||
})?;
|
||||
|
||||
// Store the session
|
||||
agent.update(cx, |agent, cx| {
|
||||
agent.sessions.insert(
|
||||
|
@ -803,7 +818,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
|
|||
log::info!("Cancelling on session: {}", session_id);
|
||||
self.0.update(cx, |agent, cx| {
|
||||
if let Some(agent) = agent.sessions.get(session_id) {
|
||||
agent.thread.update(cx, |thread, _cx| thread.cancel());
|
||||
agent.thread.update(cx, |thread, cx| thread.cancel(cx));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -830,7 +845,10 @@ struct NativeAgentSessionEditor(Entity<Thread>);
|
|||
|
||||
impl acp_thread::AgentSessionEditor for NativeAgentSessionEditor {
|
||||
fn truncate(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task<Result<()>> {
|
||||
Task::ready(self.0.update(cx, |thread, _cx| thread.truncate(message_id)))
|
||||
Task::ready(
|
||||
self.0
|
||||
.update(cx, |thread, cx| thread.truncate(message_id, cx)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -345,7 +345,7 @@ async fn test_streaming_tool_calls(cx: &mut TestAppContext) {
|
|||
|
||||
let mut saw_partial_tool_use = false;
|
||||
while let Some(event) = events.next().await {
|
||||
if let Ok(AgentResponseEvent::ToolCall(tool_call)) = event {
|
||||
if let Ok(ThreadEvent::ToolCall(tool_call)) = event {
|
||||
thread.update(cx, |thread, _cx| {
|
||||
// Look for a tool use in the thread's last message
|
||||
let message = thread.last_message().unwrap();
|
||||
|
@ -735,16 +735,14 @@ async fn test_send_after_tool_use_limit(cx: &mut TestAppContext) {
|
|||
);
|
||||
}
|
||||
|
||||
async fn expect_tool_call(
|
||||
events: &mut UnboundedReceiver<Result<AgentResponseEvent>>,
|
||||
) -> acp::ToolCall {
|
||||
async fn expect_tool_call(events: &mut UnboundedReceiver<Result<ThreadEvent>>) -> acp::ToolCall {
|
||||
let event = events
|
||||
.next()
|
||||
.await
|
||||
.expect("no tool call authorization event received")
|
||||
.unwrap();
|
||||
match event {
|
||||
AgentResponseEvent::ToolCall(tool_call) => return tool_call,
|
||||
ThreadEvent::ToolCall(tool_call) => return tool_call,
|
||||
event => {
|
||||
panic!("Unexpected event {event:?}");
|
||||
}
|
||||
|
@ -752,7 +750,7 @@ async fn expect_tool_call(
|
|||
}
|
||||
|
||||
async fn expect_tool_call_update_fields(
|
||||
events: &mut UnboundedReceiver<Result<AgentResponseEvent>>,
|
||||
events: &mut UnboundedReceiver<Result<ThreadEvent>>,
|
||||
) -> acp::ToolCallUpdate {
|
||||
let event = events
|
||||
.next()
|
||||
|
@ -760,7 +758,7 @@ async fn expect_tool_call_update_fields(
|
|||
.expect("no tool call authorization event received")
|
||||
.unwrap();
|
||||
match event {
|
||||
AgentResponseEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => {
|
||||
ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => {
|
||||
return update;
|
||||
}
|
||||
event => {
|
||||
|
@ -770,7 +768,7 @@ async fn expect_tool_call_update_fields(
|
|||
}
|
||||
|
||||
async fn next_tool_call_authorization(
|
||||
events: &mut UnboundedReceiver<Result<AgentResponseEvent>>,
|
||||
events: &mut UnboundedReceiver<Result<ThreadEvent>>,
|
||||
) -> ToolCallAuthorization {
|
||||
loop {
|
||||
let event = events
|
||||
|
@ -778,7 +776,7 @@ async fn next_tool_call_authorization(
|
|||
.await
|
||||
.expect("no tool call authorization event received")
|
||||
.unwrap();
|
||||
if let AgentResponseEvent::ToolCallAuthorization(tool_call_authorization) = event {
|
||||
if let ThreadEvent::ToolCallAuthorization(tool_call_authorization) = event {
|
||||
let permission_kinds = tool_call_authorization
|
||||
.options
|
||||
.iter()
|
||||
|
@ -945,13 +943,13 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
|||
let mut echo_completed = false;
|
||||
while let Some(event) = events.next().await {
|
||||
match event.unwrap() {
|
||||
AgentResponseEvent::ToolCall(tool_call) => {
|
||||
ThreadEvent::ToolCall(tool_call) => {
|
||||
assert_eq!(tool_call.title, expected_tools.remove(0));
|
||||
if tool_call.title == "Echo" {
|
||||
echo_id = Some(tool_call.id);
|
||||
}
|
||||
}
|
||||
AgentResponseEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
|
||||
ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
|
||||
acp::ToolCallUpdate {
|
||||
id,
|
||||
fields:
|
||||
|
@ -973,13 +971,13 @@ async fn test_cancellation(cx: &mut TestAppContext) {
|
|||
|
||||
// Cancel the current send and ensure that the event stream is closed, even
|
||||
// if one of the tools is still running.
|
||||
thread.update(cx, |thread, _cx| thread.cancel());
|
||||
thread.update(cx, |thread, cx| thread.cancel(cx));
|
||||
let events = events.collect::<Vec<_>>().await;
|
||||
let last_event = events.last();
|
||||
assert!(
|
||||
matches!(
|
||||
last_event,
|
||||
Some(Ok(AgentResponseEvent::Stop(acp::StopReason::Canceled)))
|
||||
Some(Ok(ThreadEvent::Stop(acp::StopReason::Canceled)))
|
||||
),
|
||||
"unexpected event {last_event:?}"
|
||||
);
|
||||
|
@ -1161,7 +1159,7 @@ async fn test_truncate(cx: &mut TestAppContext) {
|
|||
});
|
||||
|
||||
thread
|
||||
.update(cx, |thread, _cx| thread.truncate(message_id))
|
||||
.update(cx, |thread, cx| thread.truncate(message_id, cx))
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
thread.read_with(cx, |thread, _| {
|
||||
|
@ -1203,6 +1201,51 @@ async fn test_truncate(cx: &mut TestAppContext) {
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_title_generation(cx: &mut TestAppContext) {
|
||||
let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await;
|
||||
let fake_model = model.as_fake();
|
||||
|
||||
let summary_model = Arc::new(FakeLanguageModel::default());
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.set_summarization_model(Some(summary_model.clone()), cx)
|
||||
});
|
||||
|
||||
let send = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hello"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
fake_model.send_last_completion_stream_text_chunk("Hey!");
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "New Thread"));
|
||||
|
||||
// Ensure the summary model has been invoked to generate a title.
|
||||
summary_model.send_last_completion_stream_text_chunk("Hello ");
|
||||
summary_model.send_last_completion_stream_text_chunk("world\nG");
|
||||
summary_model.send_last_completion_stream_text_chunk("oodnight Moon");
|
||||
summary_model.end_last_completion_stream();
|
||||
send.collect::<Vec<_>>().await;
|
||||
thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world"));
|
||||
|
||||
// Send another message, ensuring no title is generated this time.
|
||||
let send = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(UserMessageId::new(), ["Hello again"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
fake_model.send_last_completion_stream_text_chunk("Hey again!");
|
||||
fake_model.end_last_completion_stream();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(summary_model.pending_completions(), Vec::new());
|
||||
send.collect::<Vec<_>>().await;
|
||||
thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "Hello world"));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||
cx.update(settings::init);
|
||||
|
@ -1442,7 +1485,7 @@ async fn test_send_no_retry_on_success(cx: &mut TestAppContext) {
|
|||
|
||||
let mut events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.set_completion_mode(agent_settings::CompletionMode::Burn);
|
||||
thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx);
|
||||
thread.send(UserMessageId::new(), ["Hello!"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
@ -1454,10 +1497,10 @@ async fn test_send_no_retry_on_success(cx: &mut TestAppContext) {
|
|||
let mut retry_events = Vec::new();
|
||||
while let Some(Ok(event)) = events.next().await {
|
||||
match event {
|
||||
AgentResponseEvent::Retry(retry_status) => {
|
||||
ThreadEvent::Retry(retry_status) => {
|
||||
retry_events.push(retry_status);
|
||||
}
|
||||
AgentResponseEvent::Stop(..) => break,
|
||||
ThreadEvent::Stop(..) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -1486,7 +1529,7 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
|
|||
|
||||
let mut events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.set_completion_mode(agent_settings::CompletionMode::Burn);
|
||||
thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx);
|
||||
thread.send(UserMessageId::new(), ["Hello!"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
@ -1507,10 +1550,10 @@ async fn test_send_retry_on_error(cx: &mut TestAppContext) {
|
|||
let mut retry_events = Vec::new();
|
||||
while let Some(Ok(event)) = events.next().await {
|
||||
match event {
|
||||
AgentResponseEvent::Retry(retry_status) => {
|
||||
ThreadEvent::Retry(retry_status) => {
|
||||
retry_events.push(retry_status);
|
||||
}
|
||||
AgentResponseEvent::Stop(..) => break,
|
||||
ThreadEvent::Stop(..) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -1543,7 +1586,7 @@ async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) {
|
|||
|
||||
let mut events = thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.set_completion_mode(agent_settings::CompletionMode::Burn);
|
||||
thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx);
|
||||
thread.send(UserMessageId::new(), ["Hello!"], cx)
|
||||
})
|
||||
.unwrap();
|
||||
|
@ -1565,10 +1608,10 @@ async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) {
|
|||
let mut retry_events = Vec::new();
|
||||
while let Some(event) = events.next().await {
|
||||
match event {
|
||||
Ok(AgentResponseEvent::Retry(retry_status)) => {
|
||||
Ok(ThreadEvent::Retry(retry_status)) => {
|
||||
retry_events.push(retry_status);
|
||||
}
|
||||
Ok(AgentResponseEvent::Stop(..)) => break,
|
||||
Ok(ThreadEvent::Stop(..)) => break,
|
||||
Err(error) => errors.push(error),
|
||||
_ => {}
|
||||
}
|
||||
|
@ -1592,11 +1635,11 @@ async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) {
|
|||
}
|
||||
|
||||
/// Filters out the stop events for asserting against in tests
|
||||
fn stop_events(result_events: Vec<Result<AgentResponseEvent>>) -> Vec<acp::StopReason> {
|
||||
fn stop_events(result_events: Vec<Result<ThreadEvent>>) -> Vec<acp::StopReason> {
|
||||
result_events
|
||||
.into_iter()
|
||||
.filter_map(|event| match event.unwrap() {
|
||||
AgentResponseEvent::Stop(stop_reason) => Some(stop_reason),
|
||||
ThreadEvent::Stop(stop_reason) => Some(stop_reason),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
|
@ -1713,6 +1756,7 @@ async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest {
|
|||
action_log,
|
||||
templates,
|
||||
Some(model.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -228,4 +228,14 @@ impl AnyAgentTool for ContextServerTool {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn replay(
|
||||
&self,
|
||||
_input: serde_json::Value,
|
||||
_output: serde_json::Value,
|
||||
_event_stream: ToolCallEventStream,
|
||||
_cx: &mut App,
|
||||
) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,10 +5,10 @@ use anyhow::{Context as _, Result, anyhow};
|
|||
use assistant_tools::edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat};
|
||||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::HashSet;
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
|
||||
use indoc::formatdoc;
|
||||
use language::ToPoint;
|
||||
use language::language_settings::{self, FormatOnSave};
|
||||
use language::{LanguageRegistry, ToPoint};
|
||||
use language_model::LanguageModelToolResultContent;
|
||||
use paths;
|
||||
use project::lsp_store::{FormatTrigger, LspFormatTarget};
|
||||
|
@ -98,11 +98,13 @@ pub enum EditFileMode {
|
|||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct EditFileToolOutput {
|
||||
#[serde(alias = "original_path")]
|
||||
input_path: PathBuf,
|
||||
project_path: PathBuf,
|
||||
new_text: String,
|
||||
old_text: Arc<String>,
|
||||
#[serde(default)]
|
||||
diff: String,
|
||||
#[serde(alias = "raw_output")]
|
||||
edit_agent_output: EditAgentOutput,
|
||||
}
|
||||
|
||||
|
@ -122,12 +124,16 @@ impl From<EditFileToolOutput> for LanguageModelToolResultContent {
|
|||
}
|
||||
|
||||
pub struct EditFileTool {
|
||||
thread: Entity<Thread>,
|
||||
thread: WeakEntity<Thread>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
}
|
||||
|
||||
impl EditFileTool {
|
||||
pub fn new(thread: Entity<Thread>) -> Self {
|
||||
Self { thread }
|
||||
pub fn new(thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>) -> Self {
|
||||
Self {
|
||||
thread,
|
||||
language_registry,
|
||||
}
|
||||
}
|
||||
|
||||
fn authorize(
|
||||
|
@ -167,8 +173,11 @@ impl EditFileTool {
|
|||
|
||||
// Check if path is inside the global config directory
|
||||
// First check if it's already inside project - if not, try to canonicalize
|
||||
let thread = self.thread.read(cx);
|
||||
let project_path = thread.project().read(cx).find_project_path(&input.path, cx);
|
||||
let Ok(project_path) = self.thread.read_with(cx, |thread, cx| {
|
||||
thread.project().read(cx).find_project_path(&input.path, cx)
|
||||
}) else {
|
||||
return Task::ready(Err(anyhow!("thread was dropped")));
|
||||
};
|
||||
|
||||
// If the path is inside the project, and it's not one of the above edge cases,
|
||||
// then no confirmation is necessary. Otherwise, confirmation is necessary.
|
||||
|
@ -221,7 +230,12 @@ impl AgentTool for EditFileTool {
|
|||
event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Self::Output>> {
|
||||
let project = self.thread.read(cx).project().clone();
|
||||
let Ok(project) = self
|
||||
.thread
|
||||
.read_with(cx, |thread, _cx| thread.project().clone())
|
||||
else {
|
||||
return Task::ready(Err(anyhow!("thread was dropped")));
|
||||
};
|
||||
let project_path = match resolve_path(&input, project.clone(), cx) {
|
||||
Ok(path) => path,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
|
@ -237,23 +251,17 @@ impl AgentTool for EditFileTool {
|
|||
});
|
||||
}
|
||||
|
||||
let Some(request) = self.thread.update(cx, |thread, cx| {
|
||||
thread
|
||||
.build_completion_request(CompletionIntent::ToolResults, cx)
|
||||
.ok()
|
||||
}) else {
|
||||
return Task::ready(Err(anyhow!("Failed to build completion request")));
|
||||
};
|
||||
let thread = self.thread.read(cx);
|
||||
let Some(model) = thread.model().cloned() else {
|
||||
return Task::ready(Err(anyhow!("No language model configured")));
|
||||
};
|
||||
let action_log = thread.action_log().clone();
|
||||
|
||||
let authorize = self.authorize(&input, &event_stream, cx);
|
||||
cx.spawn(async move |cx: &mut AsyncApp| {
|
||||
authorize.await?;
|
||||
|
||||
let (request, model, action_log) = self.thread.update(cx, |thread, cx| {
|
||||
let request = thread.build_completion_request(CompletionIntent::ToolResults, cx);
|
||||
(request, thread.model().cloned(), thread.action_log().clone())
|
||||
})?;
|
||||
let request = request?;
|
||||
let model = model.context("No language model configured")?;
|
||||
|
||||
let edit_format = EditFormat::from_model(model.clone())?;
|
||||
let edit_agent = EditAgent::new(
|
||||
model,
|
||||
|
@ -419,7 +427,6 @@ impl AgentTool for EditFileTool {
|
|||
|
||||
Ok(EditFileToolOutput {
|
||||
input_path: input.path,
|
||||
project_path: project_path.path.to_path_buf(),
|
||||
new_text: new_text.clone(),
|
||||
old_text,
|
||||
diff: unified_diff,
|
||||
|
@ -427,6 +434,25 @@ impl AgentTool for EditFileTool {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn replay(
|
||||
&self,
|
||||
_input: Self::Input,
|
||||
output: Self::Output,
|
||||
event_stream: ToolCallEventStream,
|
||||
cx: &mut App,
|
||||
) -> Result<()> {
|
||||
event_stream.update_diff(cx.new(|cx| {
|
||||
Diff::finalized(
|
||||
output.input_path,
|
||||
Some(output.old_text.to_string()),
|
||||
output.new_text,
|
||||
self.language_registry.clone(),
|
||||
cx,
|
||||
)
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that the file path is valid, meaning:
|
||||
|
@ -515,6 +541,7 @@ mod tests {
|
|||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/root", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
|
@ -527,6 +554,7 @@ mod tests {
|
|||
action_log,
|
||||
Templates::new(),
|
||||
Some(model),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
@ -537,7 +565,11 @@ mod tests {
|
|||
path: "root/nonexistent_file.txt".into(),
|
||||
mode: EditFileMode::Edit,
|
||||
};
|
||||
Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx)
|
||||
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
|
||||
input,
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
|
@ -724,6 +756,7 @@ mod tests {
|
|||
action_log.clone(),
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
@ -750,9 +783,10 @@ mod tests {
|
|||
path: "root/src/main.rs".into(),
|
||||
mode: EditFileMode::Overwrite,
|
||||
};
|
||||
Arc::new(EditFileTool {
|
||||
thread: thread.clone(),
|
||||
})
|
||||
Arc::new(EditFileTool::new(
|
||||
thread.downgrade(),
|
||||
language_registry.clone(),
|
||||
))
|
||||
.run(input, ToolCallEventStream::test().0, cx)
|
||||
});
|
||||
|
||||
|
@ -806,7 +840,11 @@ mod tests {
|
|||
path: "root/src/main.rs".into(),
|
||||
mode: EditFileMode::Overwrite,
|
||||
};
|
||||
Arc::new(EditFileTool { thread }).run(input, ToolCallEventStream::test().0, cx)
|
||||
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
|
||||
input,
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Stream the unformatted content
|
||||
|
@ -850,6 +888,7 @@ mod tests {
|
|||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
|
@ -860,6 +899,7 @@ mod tests {
|
|||
action_log.clone(),
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
@ -887,9 +927,10 @@ mod tests {
|
|||
path: "root/src/main.rs".into(),
|
||||
mode: EditFileMode::Overwrite,
|
||||
};
|
||||
Arc::new(EditFileTool {
|
||||
thread: thread.clone(),
|
||||
})
|
||||
Arc::new(EditFileTool::new(
|
||||
thread.downgrade(),
|
||||
language_registry.clone(),
|
||||
))
|
||||
.run(input, ToolCallEventStream::test().0, cx)
|
||||
});
|
||||
|
||||
|
@ -938,10 +979,11 @@ mod tests {
|
|||
path: "root/src/main.rs".into(),
|
||||
mode: EditFileMode::Overwrite,
|
||||
};
|
||||
Arc::new(EditFileTool {
|
||||
thread: thread.clone(),
|
||||
})
|
||||
.run(input, ToolCallEventStream::test().0, cx)
|
||||
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
|
||||
input,
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Stream the content with trailing whitespace
|
||||
|
@ -976,6 +1018,7 @@ mod tests {
|
|||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
|
@ -986,10 +1029,11 @@ mod tests {
|
|||
action_log.clone(),
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
fs.insert_tree("/root", json!({})).await;
|
||||
|
||||
// Test 1: Path with .zed component should require confirmation
|
||||
|
@ -1111,6 +1155,7 @@ mod tests {
|
|||
let fs = project::FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/project", json!({})).await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
|
@ -1123,10 +1168,11 @@ mod tests {
|
|||
action_log.clone(),
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
|
||||
// Test global config paths - these should require confirmation if they exist and are outside the project
|
||||
let test_cases = vec![
|
||||
|
@ -1220,7 +1266,7 @@ mod tests {
|
|||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
|
@ -1233,10 +1279,11 @@ mod tests {
|
|||
action_log.clone(),
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
|
||||
// Test files in different worktrees
|
||||
let test_cases = vec![
|
||||
|
@ -1302,6 +1349,7 @@ mod tests {
|
|||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
|
@ -1314,10 +1362,11 @@ mod tests {
|
|||
action_log.clone(),
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
|
||||
// Test edge cases
|
||||
let test_cases = vec![
|
||||
|
@ -1386,6 +1435,7 @@ mod tests {
|
|||
)
|
||||
.await;
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
|
@ -1398,10 +1448,11 @@ mod tests {
|
|||
action_log.clone(),
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
|
||||
// Test different EditFileMode values
|
||||
let modes = vec![
|
||||
|
@ -1467,6 +1518,7 @@ mod tests {
|
|||
init_test(cx);
|
||||
let fs = project::FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _cx| project.languages().clone());
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
|
@ -1479,10 +1531,11 @@ mod tests {
|
|||
action_log.clone(),
|
||||
Templates::new(),
|
||||
Some(model.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool { thread });
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
|
||||
assert_eq!(
|
||||
tool.initial_title(Err(json!({
|
||||
|
|
|
@ -319,7 +319,7 @@ mod tests {
|
|||
use theme::ThemeSettings;
|
||||
use util::test::TempTree;
|
||||
|
||||
use crate::AgentResponseEvent;
|
||||
use crate::ThreadEvent;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -396,7 +396,7 @@ mod tests {
|
|||
});
|
||||
cx.run_until_parked();
|
||||
let event = stream_rx.try_next();
|
||||
if let Ok(Some(Ok(AgentResponseEvent::ToolCallAuthorization(auth)))) = event {
|
||||
if let Ok(Some(Ok(ThreadEvent::ToolCallAuthorization(auth)))) = event {
|
||||
auth.response.send(auth.options[0].id.clone()).unwrap();
|
||||
}
|
||||
|
||||
|
|
|
@ -80,33 +80,48 @@ impl AgentTool for WebSearchTool {
|
|||
}
|
||||
};
|
||||
|
||||
let result_text = if response.results.len() == 1 {
|
||||
"1 result".to_string()
|
||||
} else {
|
||||
format!("{} results", response.results.len())
|
||||
};
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
title: Some(format!("Searched the web: {result_text}")),
|
||||
content: Some(
|
||||
response
|
||||
.results
|
||||
.iter()
|
||||
.map(|result| acp::ToolCallContent::Content {
|
||||
content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
name: result.title.clone(),
|
||||
uri: result.url.clone(),
|
||||
title: Some(result.title.clone()),
|
||||
description: Some(result.text.clone()),
|
||||
mime_type: None,
|
||||
annotations: None,
|
||||
size: None,
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
emit_update(&response, &event_stream);
|
||||
Ok(WebSearchToolOutput(response))
|
||||
})
|
||||
}
|
||||
|
||||
fn replay(
|
||||
&self,
|
||||
_input: Self::Input,
|
||||
output: Self::Output,
|
||||
event_stream: ToolCallEventStream,
|
||||
_cx: &mut App,
|
||||
) -> Result<()> {
|
||||
emit_update(&output.0, &event_stream);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream) {
|
||||
let result_text = if response.results.len() == 1 {
|
||||
"1 result".to_string()
|
||||
} else {
|
||||
format!("{} results", response.results.len())
|
||||
};
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
title: Some(format!("Searched the web: {result_text}")),
|
||||
content: Some(
|
||||
response
|
||||
.results
|
||||
.iter()
|
||||
.map(|result| acp::ToolCallContent::Content {
|
||||
content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
name: result.title.clone(),
|
||||
uri: result.url.clone(),
|
||||
title: Some(result.title.clone()),
|
||||
description: Some(result.text.clone()),
|
||||
mime_type: None,
|
||||
annotations: None,
|
||||
size: None,
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue