From 18c77b7b2839b5a0df2a1ca7393059bd57b2e13a Mon Sep 17 00:00:00 2001 From: tulsi Date: Wed, 26 Aug 2026 16:04:22 +0000 Subject: [PATCH] feat: title sessions by subject, not workflow step (#11135) Co-authored-by: Douwe Osinga --- crates/goose/src/prompts/session_name.md | 16 +++++++-- crates/goose/src/providers/cli_common.rs | 2 -- crates/goose/src/session/session_manager.rs | 10 ++++-- crates/goose/src/session/session_naming.rs | 40 ++++++--------------- 4 files changed, 32 insertions(+), 36 deletions(-) diff --git a/crates/goose/src/prompts/session_name.md b/crates/goose/src/prompts/session_name.md index 0d53d40d4..b0878055f 100644 --- a/crates/goose/src/prompts/session_name.md +++ b/crates/goose/src/prompts/session_name.md @@ -1,7 +1,19 @@ -Generate a short title (four words or less) that describes the topic of the user's messages. +Generate a short title (four words or less) for this conversation. + +Title what the work is ABOUT, not the mechanical activity. Many conversations share the same workflow steps (creating a PR, setting up a worktree, drafting an email, summarizing a document); a good title carries the distinguishing subject instead — a ticket or issue ID, feature name, customer or company, person, document, event, or project. + +Rules: +- If a ticket or issue identifier (like ABC-123) appears in the messages, include it in the title. Use identifiers found only in hints when the messages make their relevance clear. +- Prefer names of companies, projects, or documents over generic activity words. +- Prefer a company or project name over a person's name when both are present. +- If there is genuinely no distinguishing subject, a plain activity title is fine — never invent specifics that are not present. + Reply with only the title, nothing else. Do not show your reasoning. Examples: - "how do I reverse a list in python?" → Python list reversal +- "set up a git worktree for BOT-1565 session auto titles" → BOT-1565 session auto-titles +- "open the payments repo and create a PR for the refund timeout fix" → Refund timeout fix +- "help me draft a follow-up email about the renewal" (hints mention Acme) → Acme renewal follow-up +- "summarize this spreadsheet" (attached "Q3 pipeline.xlsx") → Q3 pipeline summary - "what's the weather in Tokyo?" → Tokyo weather -- "explain how transformers work in ML" → ML transformers explained \ No newline at end of file diff --git a/crates/goose/src/providers/cli_common.rs b/crates/goose/src/providers/cli_common.rs index b79905f1f..59364e9e8 100644 --- a/crates/goose/src/providers/cli_common.rs +++ b/crates/goose/src/providers/cli_common.rs @@ -57,8 +57,6 @@ pub(crate) fn generate_simple_session_description( }) }) .map(|text| { - // Strip the wrapper added by generate_session_name so we get - // the actual user content. First strip the optional background context section. let text = text .rfind(SESSION_NAME_BEGIN_MARKER) .and_then(|idx| text.get(idx..)) diff --git a/crates/goose/src/session/session_manager.rs b/crates/goose/src/session/session_manager.rs index 1c3bbc56a..f61d2473f 100644 --- a/crates/goose/src/session/session_manager.rs +++ b/crates/goose/src/session/session_manager.rs @@ -638,8 +638,14 @@ impl SessionManager { }; if should_generate_name { - let name = - generate_session_name(provider.as_ref(), &model_config, id, &conversation).await?; + let name = generate_session_name( + provider.as_ref(), + &model_config, + id, + &conversation, + Some(session.working_dir.as_path()), + ) + .await?; return Ok(Some(self.system_generated_name_update(id, name).await?)); } Ok(None) diff --git a/crates/goose/src/session/session_naming.rs b/crates/goose/src/session/session_naming.rs index 192667165..a59f7b204 100644 --- a/crates/goose/src/session/session_naming.rs +++ b/crates/goose/src/session/session_naming.rs @@ -1,3 +1,4 @@ +use std::path::Path; use std::sync::LazyLock; use anyhow::Result; @@ -70,8 +71,6 @@ fn extract_short_title(text: &str) -> String { text.to_string() } -/// Returns the first 3 user messages as strings for session naming, -/// filtering out assistant-only content (e.g. preprompt blocks). fn get_initial_user_messages(messages: &Conversation) -> Vec { messages .iter() @@ -88,36 +87,14 @@ fn get_initial_user_messages(messages: &Conversation) -> Vec { .collect() } -/// Extracts preprompt context (assistant-audience blocks) from the first user message. -/// These are content blocks visible to the assistant but not the user. -fn get_preprompt_context(messages: &Conversation) -> String { - messages - .iter() - .filter(|m| m.role == rmcp::model::Role::User) - .take(1) - .flat_map(|m| m.content.iter()) - .filter_map(|c| { - // If this block is NOT visible to the user, it's preprompt/assistant-only content - if c.filter_for_audience(rmcp::model::Role::User).is_none() { - c.as_text().map(|s| s.to_string()) - } else { - None - } - }) - .collect::>() - .join("\n") -} - -/// Generate a session name/description based on the conversation history -/// Creates a prompt asking for a concise description in 4 words or less. pub(crate) async fn generate_session_name( provider: &dyn Provider, model_config: &goose_providers::model::ModelConfig, session_id: &str, messages: &Conversation, + working_dir: Option<&Path>, ) -> Result { let context = get_initial_user_messages(messages); - let preprompt_context = get_preprompt_context(messages); let system = crate::prompt_template::render_template( "session_name.md", &std::collections::HashMap::::new(), @@ -127,18 +104,21 @@ pub(crate) async fn generate_session_name( SESSION_NAME_BEGIN_MARKER, SESSION_NAME_END_MARKER, SESSION_NAME_SUFFIX, }; - let preprompt_section = if preprompt_context.is_empty() { + let hint = working_dir + .and_then(Path::file_name) + .and_then(|folder| folder.to_str()) + .map(|folder| format!("working folder: {folder}")) + .unwrap_or_default(); + let hints_section = if hint.is_empty() { String::new() } else { format!( - "---BEGIN BACKGROUND CONTEXT (for understanding only, do NOT base the title on this)---\n{}\n---END BACKGROUND CONTEXT---\n\n", - preprompt_context + "---BEGIN HINTS (optional signals like the working folder; use them only when they match the subject of the messages)---\n{hint}\n---END HINTS---\n\n" ) }; - let user_text = format!( "{}{}\n{}\n{}\n\n{}", - preprompt_section, + hints_section, SESSION_NAME_BEGIN_MARKER, context.join("\n"), SESSION_NAME_END_MARKER,