feat: title sessions by subject, not workflow step (#11135)
Co-authored-by: Douwe Osinga <douwe.osinga@gmail.com>
This commit is contained in:
@@ -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
|
||||
@@ -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..))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user