feat: title sessions by subject, not workflow step (#11135)

Co-authored-by: Douwe Osinga <douwe.osinga@gmail.com>
This commit is contained in:
tulsi
2026-08-26 16:04:22 +00:00
committed by GitHub
parent b9b671c6c3
commit 18c77b7b28
4 changed files with 32 additions and 36 deletions
+14 -2
View File
@@ -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
-2
View File
@@ -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..))
+8 -2
View File
@@ -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)
+10 -30
View File
@@ -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<String> {
messages
.iter()
@@ -88,36 +87,14 @@ fn get_initial_user_messages(messages: &Conversation) -> Vec<String> {
.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::<Vec<_>>()
.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<String> {
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::<String, String>::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,