fix: SACP notifies clients of generated session names (#8983)
Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
@@ -43,12 +43,13 @@ use sacp::schema::{
|
||||
PermissionOption, PermissionOptionKind, PromptCapabilities, PromptRequest, PromptResponse,
|
||||
RequestPermissionOutcome, RequestPermissionRequest, ResourceLink, SessionCapabilities,
|
||||
SessionCloseCapabilities, SessionConfigOption, SessionConfigOptionCategory,
|
||||
SessionConfigSelectOption, SessionId, SessionInfo, SessionListCapabilities, SessionMode,
|
||||
SessionModeId, SessionModeState, SessionModelState, SessionNotification, SessionUpdate,
|
||||
SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, SetSessionModeRequest,
|
||||
SetSessionModeResponse, SetSessionModelRequest, SetSessionModelResponse, StopReason,
|
||||
TextContent, TextResourceContents, ToolCall, ToolCallContent, ToolCallId, ToolCallLocation,
|
||||
ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields, ToolKind, Usage, UsageUpdate,
|
||||
SessionConfigSelectOption, SessionId, SessionInfo, SessionInfoUpdate, SessionListCapabilities,
|
||||
SessionMode, SessionModeId, SessionModeState, SessionModelState, SessionNotification,
|
||||
SessionUpdate, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse,
|
||||
SetSessionModeRequest, SetSessionModeResponse, SetSessionModelRequest, SetSessionModelResponse,
|
||||
StopReason, TextContent, TextResourceContents, ToolCall, ToolCallContent, ToolCallId,
|
||||
ToolCallLocation, ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields, ToolKind, Usage,
|
||||
UsageUpdate,
|
||||
};
|
||||
use sacp::util::MatchDispatchFrom;
|
||||
use sacp::{
|
||||
@@ -268,6 +269,36 @@ fn thread_session_meta(
|
||||
meta
|
||||
}
|
||||
|
||||
fn spawn_session_name_update_notifier(
|
||||
cx: ConnectionTo<Client>,
|
||||
) -> tokio::sync::mpsc::UnboundedSender<crate::session::SessionNameUpdate> {
|
||||
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::<crate::session::SessionNameUpdate>();
|
||||
tokio::spawn(async move {
|
||||
while let Some(update) = rx.recv().await {
|
||||
let thread = update.thread;
|
||||
let thread_id = thread.id.clone();
|
||||
let meta = thread_session_meta(&thread);
|
||||
let notification = SessionNotification::new(
|
||||
SessionId::new(thread_id.clone()),
|
||||
SessionUpdate::SessionInfoUpdate(
|
||||
SessionInfoUpdate::new()
|
||||
.title(thread.name)
|
||||
.updated_at(thread.updated_at.to_rfc3339())
|
||||
.meta(meta),
|
||||
),
|
||||
);
|
||||
if let Err(error) = cx.send_notification(notification) {
|
||||
warn!(
|
||||
thread_id = %thread_id,
|
||||
error = %error,
|
||||
"Failed to send generated session name update"
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
tx
|
||||
}
|
||||
|
||||
fn extract_timeout_from_meta(meta: &Option<Meta>) -> Option<u64> {
|
||||
meta.as_ref()
|
||||
.and_then(|m| m.get("timeout"))
|
||||
@@ -1147,6 +1178,9 @@ impl GooseAcpAgent {
|
||||
}
|
||||
};
|
||||
|
||||
let session_name_update_tx =
|
||||
(!disable_session_naming).then(|| spawn_session_name_update_notifier(cx.clone()));
|
||||
|
||||
// ── Phase 1: create agent + init provider (fast, ~55ms) ──────
|
||||
let phase1: Result<Arc<Agent>, String> = async {
|
||||
let agent = Arc::new(Agent::with_config(
|
||||
@@ -1158,7 +1192,8 @@ impl GooseAcpAgent {
|
||||
disable_session_naming,
|
||||
goose_platform,
|
||||
)
|
||||
.with_mcp_host_info(client_mcp_host_info),
|
||||
.with_mcp_host_info(client_mcp_host_info)
|
||||
.with_session_name_update_tx(session_name_update_tx),
|
||||
));
|
||||
|
||||
// Init provider — reuse the pre-resolved name + model when
|
||||
|
||||
@@ -143,10 +143,20 @@ impl GooseAcpAgent {
|
||||
&self,
|
||||
req: RenameSessionRequest,
|
||||
) -> Result<EmptyResponse, sacp::Error> {
|
||||
self.thread_manager
|
||||
.update_thread(&req.session_id, Some(req.title), Some(true), None)
|
||||
let title = req.title;
|
||||
let thread = self
|
||||
.thread_manager
|
||||
.update_thread(&req.session_id, Some(title.clone()), Some(true), None)
|
||||
.await
|
||||
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
|
||||
if let Some(internal_session_id) = thread.current_session_id {
|
||||
self.session_manager
|
||||
.update(&internal_session_id)
|
||||
.user_provided_name(title)
|
||||
.apply()
|
||||
.await
|
||||
.map_err(|e| sacp::Error::internal_error().data(e.to_string()))?;
|
||||
}
|
||||
Ok(EmptyResponse {})
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ use crate::security::adversary_inspector::AdversaryInspector;
|
||||
use crate::security::egress_inspector::EgressInspector;
|
||||
use crate::security::security_inspector::SecurityInspector;
|
||||
use crate::session::extension_data::{EnabledExtensionsState, ExtensionState};
|
||||
use crate::session::{Session, SessionManager};
|
||||
use crate::session::{Session, SessionManager, SessionNameUpdate};
|
||||
use crate::tool_inspection::ToolInspectionManager;
|
||||
use crate::tool_monitor::RepetitionInspector;
|
||||
use crate::utils::is_token_cancelled;
|
||||
@@ -116,6 +116,7 @@ pub struct AgentConfig {
|
||||
pub disable_session_naming: bool,
|
||||
pub goose_platform: GoosePlatform,
|
||||
pub mcp_host_info: Option<GooseMcpHostInfo>,
|
||||
pub session_name_update_tx: Option<mpsc::UnboundedSender<SessionNameUpdate>>,
|
||||
}
|
||||
|
||||
impl AgentConfig {
|
||||
@@ -135,6 +136,7 @@ impl AgentConfig {
|
||||
disable_session_naming,
|
||||
goose_platform,
|
||||
mcp_host_info: None,
|
||||
session_name_update_tx: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +144,14 @@ impl AgentConfig {
|
||||
self.mcp_host_info = mcp_host_info;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_session_name_update_tx(
|
||||
mut self,
|
||||
tx: Option<mpsc::UnboundedSender<SessionNameUpdate>>,
|
||||
) -> Self {
|
||||
self.session_name_update_tx = tx;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// The main goose Agent
|
||||
@@ -1247,12 +1257,21 @@ impl Agent {
|
||||
let session_id = session_config.id.clone();
|
||||
if !self.config.disable_session_naming {
|
||||
let manager_for_spawn = session_manager.clone();
|
||||
let session_name_update_tx = self.config.session_name_update_tx.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = manager_for_spawn
|
||||
match manager_for_spawn
|
||||
.maybe_update_name(&session_id, provider)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to generate session description: {}", e);
|
||||
Ok(Some(update)) => {
|
||||
if let Some(tx) = session_name_update_tx {
|
||||
if tx.send(update).is_err() {
|
||||
warn!("Failed to publish generated session name");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => warn!("Failed to generate session description: {}", e),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,6 +11,6 @@ pub use diagnostics::{
|
||||
};
|
||||
pub use extension_data::{EnabledExtensionsState, ExtensionData, ExtensionState, TodoState};
|
||||
pub use session_manager::{
|
||||
Session, SessionInsights, SessionManager, SessionType, SessionUpdateBuilder,
|
||||
Session, SessionInsights, SessionManager, SessionNameUpdate, SessionType, SessionUpdateBuilder,
|
||||
};
|
||||
pub use thread_manager::{Thread, ThreadManager, ThreadMetadata};
|
||||
|
||||
@@ -256,6 +256,11 @@ pub struct SessionManager {
|
||||
storage: Arc<SessionStorage>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SessionNameUpdate {
|
||||
pub thread: super::thread_manager::Thread,
|
||||
}
|
||||
|
||||
impl SessionManager {
|
||||
pub fn new(data_dir: PathBuf) -> Self {
|
||||
Self {
|
||||
@@ -351,11 +356,15 @@ impl SessionManager {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn maybe_update_name(&self, id: &str, provider: Arc<dyn Provider>) -> Result<()> {
|
||||
pub async fn maybe_update_name(
|
||||
&self,
|
||||
id: &str,
|
||||
provider: Arc<dyn Provider>,
|
||||
) -> Result<Option<SessionNameUpdate>> {
|
||||
let session = self.get_session(id, true).await?;
|
||||
|
||||
if session.user_set_name {
|
||||
return Ok(());
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let conversation = session
|
||||
@@ -379,15 +388,16 @@ impl SessionManager {
|
||||
if let Some(ref thread_id) = session.thread_id {
|
||||
let thread_mgr = super::thread_manager::ThreadManager::new(self.storage.clone());
|
||||
let thread = thread_mgr.get_thread(thread_id).await?;
|
||||
if !thread.user_set_name {
|
||||
thread_mgr
|
||||
if !thread.user_set_name && thread.name != name {
|
||||
let thread = thread_mgr
|
||||
.update_thread(thread_id, Some(name), Some(false), None)
|
||||
.await?;
|
||||
return Ok(Some(SessionNameUpdate { thread }));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(())
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user