ACP history mentions (#36551)

- **TEMP**
- **Update @-mentions to use new history**

Closes #ISSUE

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2025-08-20 00:25:07 -06:00 committed by GitHub
parent 159b5e9fb5
commit 5d2bb2466e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 581 additions and 392 deletions

View file

@ -8,6 +8,9 @@ license = "GPL-3.0-or-later"
[lib]
path = "src/agent2.rs"
[features]
test-support = ["db/test-support"]
[lints]
workspace = true
@ -72,6 +75,7 @@ ctor.workspace = true
client = { workspace = true, "features" = ["test-support"] }
clock = { workspace = true, "features" = ["test-support"] }
context_server = { workspace = true, "features" = ["test-support"] }
db = { workspace = true, "features" = ["test-support"] }
editor = { workspace = true, "features" = ["test-support"] }
env_logger.workspace = true
fs = { workspace = true, "features" = ["test-support"] }

View file

@ -536,6 +536,28 @@ impl NativeAgent {
})
}
pub fn thread_summary(
&mut self,
id: acp::SessionId,
cx: &mut Context<Self>,
) -> Task<Result<SharedString>> {
let thread = self.open_thread(id.clone(), cx);
cx.spawn(async move |this, cx| {
let acp_thread = thread.await?;
let result = this
.update(cx, |this, cx| {
this.sessions
.get(&id)
.unwrap()
.thread
.update(cx, |thread, cx| thread.summary(cx))
})?
.await?;
drop(acp_thread);
Ok(result)
})
}
fn save_thread(&mut self, thread: Entity<Thread>, cx: &mut Context<Self>) {
let database_future = ThreadsDatabase::connect(cx);
let (id, db_thread) =

View file

@ -1,6 +1,6 @@
use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent};
use acp_thread::UserMessageId;
use agent::thread_store;
use agent::{thread::DetailedSummaryState, thread_store};
use agent_client_protocol as acp;
use agent_settings::{AgentProfileId, CompletionMode};
use anyhow::{Result, anyhow};
@ -20,7 +20,7 @@ use std::sync::Arc;
use ui::{App, SharedString};
pub type DbMessage = crate::Message;
pub type DbSummary = agent::thread::DetailedSummaryState;
pub type DbSummary = DetailedSummaryState;
pub type DbLanguageModel = thread_store::SerializedLanguageModel;
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -37,7 +37,7 @@ pub struct DbThread {
pub messages: Vec<DbMessage>,
pub updated_at: DateTime<Utc>,
#[serde(default)]
pub summary: DbSummary,
pub detailed_summary: Option<SharedString>,
#[serde(default)]
pub initial_project_snapshot: Option<Arc<agent::thread::ProjectSnapshot>>,
#[serde(default)]
@ -185,7 +185,12 @@ impl DbThread {
title: thread.summary,
messages,
updated_at: thread.updated_at,
summary: thread.detailed_summary_state,
detailed_summary: match thread.detailed_summary_state {
DetailedSummaryState::NotGenerated | DetailedSummaryState::Generating { .. } => {
None
}
DetailedSummaryState::Generated { text, .. } => Some(text),
},
initial_project_snapshot: thread.initial_project_snapshot,
cumulative_token_usage: thread.cumulative_token_usage,
request_token_usage,

View file

@ -1,7 +1,8 @@
use crate::{DbThreadMetadata, ThreadsDatabase};
use acp_thread::MentionUri;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result, anyhow};
use assistant_context::SavedContextMetadata;
use assistant_context::{AssistantContext, SavedContextMetadata};
use chrono::{DateTime, Utc};
use db::kvp::KEY_VALUE_STORE;
use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*};
@ -38,6 +39,19 @@ impl HistoryEntry {
}
}
pub fn mention_uri(&self) -> MentionUri {
match self {
HistoryEntry::AcpThread(thread) => MentionUri::Thread {
id: thread.id.clone(),
name: thread.title.to_string(),
},
HistoryEntry::TextThread(context) => MentionUri::TextThread {
path: context.path.as_ref().to_owned(),
name: context.title.to_string(),
},
}
}
pub fn title(&self) -> &SharedString {
match self {
HistoryEntry::AcpThread(thread) if thread.title.is_empty() => DEFAULT_TITLE,
@ -48,7 +62,7 @@ impl HistoryEntry {
}
/// Generic identifier for a history entry.
#[derive(Clone, PartialEq, Eq, Debug)]
#[derive(Clone, PartialEq, Eq, Debug, Hash)]
pub enum HistoryEntryId {
AcpThread(acp::SessionId),
TextThread(Arc<Path>),
@ -120,6 +134,16 @@ impl HistoryStore {
})
}
pub fn load_text_thread(
&self,
path: Arc<Path>,
cx: &mut Context<Self>,
) -> Task<Result<Entity<AssistantContext>>> {
self.context_store.update(cx, |context_store, cx| {
context_store.open_local_context(path, cx)
})
}
pub fn reload(&self, cx: &mut Context<Self>) {
let database_future = ThreadsDatabase::connect(cx);
cx.spawn(async move |this, cx| {
@ -149,7 +173,7 @@ impl HistoryStore {
.detach_and_log_err(cx);
}
pub fn entries(&self, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
pub fn entries(&self, cx: &App) -> Vec<HistoryEntry> {
let mut history_entries = Vec::new();
#[cfg(debug_assertions)]
@ -180,10 +204,6 @@ impl HistoryStore {
.is_none()
}
pub fn recent_entries(&self, limit: usize, cx: &mut Context<Self>) -> Vec<HistoryEntry> {
self.entries(cx).into_iter().take(limit).collect()
}
pub fn recently_opened_entries(&self, cx: &App) -> Vec<HistoryEntry> {
#[cfg(debug_assertions)]
if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() {
@ -246,6 +266,10 @@ impl HistoryStore {
cx.background_executor()
.timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE)
.await;
if cfg!(any(feature = "test-support", test)) {
return;
}
KEY_VALUE_STORE
.write_kvp(RECENTLY_OPENED_THREADS_KEY.to_owned(), content)
.await
@ -255,6 +279,9 @@ impl HistoryStore {
fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> {
cx.background_spawn(async move {
if cfg!(any(feature = "test-support", test)) {
anyhow::bail!("history store does not persist in tests");
}
let json = KEY_VALUE_STORE
.read_kvp(RECENTLY_OPENED_THREADS_KEY)?
.unwrap_or("[]".to_string());

View file

@ -6,9 +6,12 @@ use crate::{
};
use acp_thread::{MentionUri, UserMessageId};
use action_log::ActionLog;
use agent::thread::{DetailedSummaryState, GitState, ProjectSnapshot, WorktreeSnapshot};
use agent::thread::{GitState, ProjectSnapshot, WorktreeSnapshot};
use agent_client_protocol as acp;
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_PROMPT};
use agent_settings::{
AgentProfileId, AgentSettings, CompletionMode, SUMMARIZE_THREAD_DETAILED_PROMPT,
SUMMARIZE_THREAD_PROMPT,
};
use anyhow::{Context as _, Result, anyhow};
use assistant_tool::adapt_schema_to_format;
use chrono::{DateTime, Utc};
@ -499,8 +502,7 @@ pub struct Thread {
prompt_id: PromptId,
updated_at: DateTime<Utc>,
title: Option<SharedString>,
#[allow(unused)]
summary: DetailedSummaryState,
summary: Option<SharedString>,
messages: Vec<Message>,
completion_mode: CompletionMode,
/// Holds the task that handles agent interaction until the end of the turn.
@ -541,7 +543,7 @@ impl Thread {
prompt_id: PromptId::new(),
updated_at: Utc::now(),
title: None,
summary: DetailedSummaryState::default(),
summary: None,
messages: Vec::new(),
completion_mode: AgentSettings::get_global(cx).preferred_completion_mode,
running_turn: None,
@ -691,7 +693,7 @@ impl Thread {
} else {
Some(db_thread.title.clone())
},
summary: db_thread.summary,
summary: db_thread.detailed_summary,
messages: db_thread.messages,
completion_mode: db_thread.completion_mode.unwrap_or_default(),
running_turn: None,
@ -719,7 +721,7 @@ impl Thread {
title: self.title.clone().unwrap_or_default(),
messages: self.messages.clone(),
updated_at: self.updated_at,
summary: self.summary.clone(),
detailed_summary: self.summary.clone(),
initial_project_snapshot: None,
cumulative_token_usage: self.cumulative_token_usage,
request_token_usage: self.request_token_usage.clone(),
@ -976,7 +978,7 @@ impl Thread {
Message::Agent(_) | Message::Resume => {}
}
}
self.summary = None;
cx.notify();
Ok(())
}
@ -1047,6 +1049,7 @@ impl Thread {
let event_stream = ThreadEventStream(events_tx);
let message_ix = self.messages.len().saturating_sub(1);
self.tool_use_limit_reached = false;
self.summary = None;
self.running_turn = Some(RunningTurn {
event_stream: event_stream.clone(),
_task: cx.spawn(async move |this, cx| {
@ -1507,6 +1510,63 @@ impl Thread {
self.title.clone().unwrap_or("New Thread".into())
}
pub fn summary(&mut self, cx: &mut Context<Self>) -> Task<Result<SharedString>> {
if let Some(summary) = self.summary.as_ref() {
return Task::ready(Ok(summary.clone()));
}
let Some(model) = self.summarization_model.clone() else {
return Task::ready(Err(anyhow!("No summarization model available")));
};
let mut request = LanguageModelRequest {
intent: Some(CompletionIntent::ThreadSummarization),
temperature: AgentSettings::temperature_for_model(&model, cx),
..Default::default()
};
for message in &self.messages {
request.messages.extend(message.to_request());
}
request.messages.push(LanguageModelRequestMessage {
role: Role::User,
content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()],
cache: false,
});
cx.spawn(async move |this, cx| {
let mut summary = String::new();
let mut messages = model.stream_completion(request, cx).await?;
while let Some(event) = messages.next().await {
let event = event?;
let text = match event {
LanguageModelCompletionEvent::Text(text) => text,
LanguageModelCompletionEvent::StatusUpdate(
CompletionRequestStatus::UsageUpdated { .. },
) => {
// this.update(cx, |thread, cx| {
// thread.update_model_request_usage(amount as u32, limit, cx);
// })?;
// TODO: handle usage update
continue;
}
_ => continue,
};
let mut lines = text.lines();
summary.extend(lines.next());
}
log::info!("Setting summary: {}", summary);
let summary = SharedString::from(summary);
this.update(cx, |this, cx| {
this.summary = Some(summary.clone());
cx.notify()
})?;
Ok(summary)
})
}
fn update_title(
&mut self,
event_stream: &ThreadEventStream,
@ -1617,6 +1677,7 @@ impl Thread {
self.messages.push(Message::Agent(message));
self.updated_at = Utc::now();
self.summary = None;
cx.notify()
}