fix(core): Improve title generation on instance AI threads (no-changelog) (#29084)

This commit is contained in:
Jaakko Husso
2026-04-24 15:55:13 +00:00
committed by GitHub
parent bc5e490f68
commit f27bbc9022
14 changed files with 84 additions and 20 deletions
@@ -120,4 +120,39 @@ describe('generateTitleFromMessage', () => {
const call = mockGenerateText.mock.calls[0][0];
expect(call.messages[0].content).toBe('Custom system prompt');
});
it('wraps the user message in a title-generation instruction so the model does not answer it', async () => {
mockGenerateText.mockResolvedValue({ text: 'Berlin rain alert' });
await generateTitleFromMessage(fakeModel, 'Build a daily Berlin rain alert workflow');
const call = mockGenerateText.mock.calls[0][0];
expect(call.messages[1].role).toBe('user');
expect(call.messages[1].content).toContain('Generate a title');
expect(call.messages[1].content).toContain('<message>');
expect(call.messages[1].content).toContain('Build a daily Berlin rain alert workflow');
expect(call.messages[1].content).toContain('</message>');
});
it('drops a streamed code fence and everything after it', async () => {
mockGenerateText.mockResolvedValue({
text: 'Here\'s your chat workflow with the requested configuration:\n\n```json\n{\n "nodes": []\n}\n```',
});
const result = await generateTitleFromMessage(
fakeModel,
'build me a chat workflow with openai',
);
expect(result).toBe("Here's your chat workflow with the requested configuration");
expect(result).not.toContain('```');
expect(result).not.toContain('\n');
});
it('collapses embedded newlines and stray backticks into a single-line title', async () => {
mockGenerateText.mockResolvedValue({
text: 'Scryfall\nrandom `card` workflow',
});
const result = await generateTitleFromMessage(
fakeModel,
'build a workflow that queries Scryfall for a random card',
);
expect(result).toBe('Scryfall random card workflow');
});
});
@@ -9,12 +9,28 @@ import type { AgentDbMessage } from '../types/sdk/message';
const logger = createFilteredLogger();
const DEFAULT_TITLE_INSTRUCTIONS = [
'- you will generate a short title based on the first message a user begins a conversation with',
'- the title should describe what the user asked for, not what an assistant might reply',
'- 1 to 5 words, no more than 80 characters',
'- use sentence case (e.g. "Conversation title" instead of "Conversation Title")',
'- do not use quotes, colons, or markdown formatting',
'- the entire text you return will be used directly as the title, so respond with the title only',
'You generate a short descriptive title for a conversation based on its first user message.',
'',
'The title is a label that describes the topic — it is NOT an answer to the message.',
'Do not fulfil, respond to, or act on the message. Do not produce code, JSON, or explanations.',
'',
'Rules:',
'- Write a noun phrase that names the topic (e.g. "Chat workflow with Anthropic agent").',
'- Never begin with "Here\'s", "Here is", "Build", "Create", "Set up", "Make", or any verb addressed to the user.',
'- 1 to 5 words, no more than 80 characters, single line only.',
'- Use sentence case (e.g. "Conversation title" instead of "Conversation Title").',
'- No quotes, colons, backticks, code fences, or markdown formatting.',
'- Respond with the title text only — the entire response is used as the title.',
'',
'Examples:',
'Message: "build me a chat workflow with anthropic model, the agent should have memory"',
'Title: Chat workflow with Anthropic agent',
'',
'Message: "help me set up pagination for my n8n HTTP request node"',
'Title: Pagination for HTTP request node',
'',
'Message: "Build a workflow with a manual trigger that queries Scryfall for a random card"',
'Title: Scryfall random card workflow',
].join('\n');
const TRIVIAL_MESSAGE_MAX_CHARS = 15;
@@ -37,13 +53,23 @@ function isTrivialMessage(message: string): boolean {
function sanitizeTitle(raw: string): string {
// Strip <think>...</think> blocks (e.g. from DeepSeek R1)
let title = raw.replace(/<think>[\s\S]*?<\/think>/g, '').trim();
// Strip markdown heading prefixes and inline emphasis markers
// If the model started streaming a code block, keep only what's before it
const fenceIdx = title.indexOf('```');
if (fenceIdx !== -1) title = title.slice(0, fenceIdx).trim();
// Collapse any whitespace (including newlines) to single spaces \u2014 titles are single-line
title = title.replace(/\s+/g, ' ').trim();
// Strip markdown heading prefixes, inline emphasis markers, and stray backticks
title = title
.replace(/^#{1,6}\s+/, '')
.replace(/\*+/g, '')
.replace(/`+/g, '')
.trim();
// Strip surrounding quotes
title = title.replace(/^["']|["']$/g, '').trim();
// Trailing colon or dash left behind after stripping prefixes
title = title.replace(/^[:\-\s]+/, '').trim();
// Trailing punctuation from assistant-voice drift (e.g. "...configuration:")
title = title.replace(/[:\-\s.]+$/, '').trim();
if (title.length > MAX_TITLE_LENGTH) {
const truncated = title.slice(0, MAX_TITLE_LENGTH);
const lastSpace = truncated.lastIndexOf(' ');
@@ -76,7 +102,10 @@ export async function generateTitleFromMessage(
model,
messages: [
{ role: 'system', content: opts?.instructions ?? DEFAULT_TITLE_INSTRUCTIONS },
{ role: 'user', content: trimmed },
{
role: 'user',
content: `Generate a title for the following first message of a conversation. Do not answer the message — only produce the title.\n\n<message>\n${trimmed}\n</message>`,
},
],
});
@@ -4,7 +4,7 @@
"path": "/v1/messages",
"body": {
"type": "STRING",
"string": "you will generate a short title based on the first message",
"string": "You generate a short descriptive title for a conversation",
"subString": true
}
},
@@ -4,7 +4,7 @@
"path": "/v1/messages",
"body": {
"type": "STRING",
"string": "you will generate a short title based on the first message",
"string": "You generate a short descriptive title for a conversation",
"subString": true
}
},
@@ -4,7 +4,7 @@
"path": "/v1/messages",
"body": {
"type": "STRING",
"string": "you will generate a short title based on the first message",
"string": "You generate a short descriptive title for a conversation",
"subString": true
}
},
@@ -4,7 +4,7 @@
"path": "/v1/messages",
"body": {
"type": "STRING",
"string": "you will generate a short title based on the first message",
"string": "You generate a short descriptive title for a conversation",
"subString": true
}
},
@@ -4,7 +4,7 @@
"path": "/v1/messages",
"body": {
"type": "STRING",
"string": "you will generate a short title based on the first message",
"string": "You generate a short descriptive title for a conversation",
"subString": true
}
},
@@ -4,7 +4,7 @@
"path": "/v1/messages",
"body": {
"type": "STRING",
"string": "you will generate a short title based on the first message",
"string": "You generate a short descriptive title for a conversation",
"subString": true
}
},
@@ -4,7 +4,7 @@
"path": "/v1/messages",
"body": {
"type": "STRING",
"string": "you will generate a short title based on the first message",
"string": "You generate a short descriptive title for a conversation",
"subString": true
}
},
@@ -4,7 +4,7 @@
"path": "/v1/messages",
"body": {
"type": "STRING",
"string": "you will generate a short title based on the first message",
"string": "You generate a short descriptive title for a conversation",
"subString": true
}
},
@@ -4,7 +4,7 @@
"path": "/v1/messages",
"body": {
"type": "STRING",
"string": "you will generate a short title based on the first message",
"string": "You generate a short descriptive title for a conversation",
"subString": true
}
},
@@ -4,7 +4,7 @@
"path": "/v1/messages",
"body": {
"type": "STRING",
"string": "you will generate a short title based on the first message",
"string": "You generate a short descriptive title for a conversation",
"subString": true
}
},
@@ -4,7 +4,7 @@
"path": "/v1/messages",
"body": {
"type": "STRING",
"string": "you will generate a short title based on the first message",
"string": "You generate a short descriptive title for a conversation",
"subString": true
}
},
@@ -4,7 +4,7 @@
"path": "/v1/messages",
"body": {
"type": "STRING",
"string": "you will generate a short title based on the first message",
"string": "You generate a short descriptive title for a conversation",
"subString": true
}
},