Skip to content

feat(sdk): support custom title generation prompts - #4564

Open
DevinVinson wants to merge 7 commits into
mainfrom
dv/custom-title-generation-prompt
Open

DevinVinson wants to merge 7 commits into
mainfrom
dv/custom-title-generation-prompt

Conversation

@DevinVinson

@DevinVinson DevinVinson commented Aug 20, 2026 •

Copy link
Copy Markdown
Member

HUMAN: Will need some 👀 to check but hopefully this is the right direction.


AGENT:

Implemented and exercised the complete Agent Server path: request validation and
conversation metadata persistence, AutoTitleSubscriber forwarding, prompt rendering,
and the LLM completion message. A live provider-backed run was not performed because
this environment has no LLM credentials; the PR remains a draft for human E2E testing.

Why

Users can choose a title LLM profile, but cannot customize the prompt used to generate
automatic conversation titles. This adds the Agent Server contract required by
#4561 and OpenHands/OpenHands#16761.

Summary

  • Add an optional, persisted title_generation_prompt conversation setting with a 2,000-character limit.
  • Render {conversation_content} and {max_length} placeholders while safely appending conversation content when its placeholder is omitted.
  • Preserve byte-for-byte default prompt behavior for unset or blank overrides and cover request-to-LLM forwarding and metadata round trips.

REST API contract changes

Compared with base OpenAPI 1de2e6d1bfcf for public /api/** paths.

--- base public OpenAPI
+++ head public OpenAPI
@@ -2306,0 +2307 @@
+schema StartConversationRequest property prompt optional schema=anyOf=[type="string" maxLength=2000,type="null"]

Issue Number

Closes #4561

How to Test

  1. Run uv run pytest tests/sdk/conversation/test_generate_title.py tests/agent_server/test_conversation_service.py tests/agent_server/test_event_service.py.
  2. Confirm all 237 tests pass.
  3. Start a conversation with autotitle: true and a prompt such as Write a title under {max_length} characters for: {conversation_content}; after the first user message, confirm the completion receives the rendered custom user prompt and meta.json retains the template.

Validation performed:

  • make build
  • Pre-commit hooks on every edited file
  • Focused suite above: 237 passed
  • Commit hooks: Ruff format/lint, pycodestyle, Pyright, import rules, and registration checks passed

Video/Screenshots

Not applicable to this backend-only change. Live LLM E2E evidence is pending human testing.

Design Doc

Not included; the API behavior and fallback rules are documented in the companion docs PR linked below.

Type

  • Bug fix
  • Feature
  • Refactor
  • Breaking change
  • Docs / chore

Notes


🐳 Agent Server images for this PR — GHCR package, pull/run commands, and all pushed tags (click to expand)

• GHCR package: https://github.com/OpenHands/agent-sdk/pkgs/container/agent-server

Variants & Base Images

Variant Architectures Base Image Docs / Tags
java amd64, arm64 eclipse-temurin:17-jdk Link
python amd64, arm64 nikolaik/python-nodejs:python3.13-nodejs22-slim Link
golang amd64, arm64 golang:1.21-bookworm Link

Pull (multi-arch manifest)

# Each variant is a multi-arch manifest supporting both amd64 and arm64
docker pull ghcr.io/openhands/agent-server:4afe9a9-python

Run

docker run -it --rm \
  -p 8000:8000 \
  --name agent-server-4afe9a9-python \
  ghcr.io/openhands/agent-server:4afe9a9-python

All tags pushed for this build

ghcr.io/openhands/agent-server:4afe9a9-golang-amd64
ghcr.io/openhands/agent-server:4afe9a979fda23b125388e898d8c3f7895f2696e-golang-amd64
ghcr.io/openhands/agent-server:dv-custom-title-generation-prompt-golang-amd64
ghcr.io/openhands/agent-server:4afe9a9-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:4afe9a9-golang-arm64
ghcr.io/openhands/agent-server:4afe9a979fda23b125388e898d8c3f7895f2696e-golang-arm64
ghcr.io/openhands/agent-server:dv-custom-title-generation-prompt-golang-arm64
ghcr.io/openhands/agent-server:4afe9a9-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:4afe9a9-java-amd64
ghcr.io/openhands/agent-server:4afe9a979fda23b125388e898d8c3f7895f2696e-java-amd64
ghcr.io/openhands/agent-server:dv-custom-title-generation-prompt-java-amd64
ghcr.io/openhands/agent-server:4afe9a9-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:4afe9a9-java-arm64
ghcr.io/openhands/agent-server:4afe9a979fda23b125388e898d8c3f7895f2696e-java-arm64
ghcr.io/openhands/agent-server:dv-custom-title-generation-prompt-java-arm64
ghcr.io/openhands/agent-server:4afe9a9-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:4afe9a9-python-amd64
ghcr.io/openhands/agent-server:4afe9a979fda23b125388e898d8c3f7895f2696e-python-amd64
ghcr.io/openhands/agent-server:dv-custom-title-generation-prompt-python-amd64
ghcr.io/openhands/agent-server:4afe9a9-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:4afe9a9-python-arm64
ghcr.io/openhands/agent-server:4afe9a979fda23b125388e898d8c3f7895f2696e-python-arm64
ghcr.io/openhands/agent-server:dv-custom-title-generation-prompt-python-arm64
ghcr.io/openhands/agent-server:4afe9a9-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:4afe9a9-golang
ghcr.io/openhands/agent-server:4afe9a979fda23b125388e898d8c3f7895f2696e-golang
ghcr.io/openhands/agent-server:dv-custom-title-generation-prompt-golang
ghcr.io/openhands/agent-server:4afe9a9-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:4afe9a9-java
ghcr.io/openhands/agent-server:4afe9a979fda23b125388e898d8c3f7895f2696e-java
ghcr.io/openhands/agent-server:dv-custom-title-generation-prompt-java
ghcr.io/openhands/agent-server:4afe9a9-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:4afe9a9-python
ghcr.io/openhands/agent-server:4afe9a979fda23b125388e898d8c3f7895f2696e-python
ghcr.io/openhands/agent-server:dv-custom-title-generation-prompt-python
ghcr.io/openhands/agent-server:4afe9a9-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 4afe9a9-python) is a multi-arch manifest supporting both amd64 and arm64
  • Docker automatically pulls the correct architecture for your platform
  • Individual architecture tags (e.g., 4afe9a9-python-amd64) are also available if needed

Jev-Fast-Audit

⚡ Jev fast audit · estimates · 0.68s · commit 4afe9a9
Strongest signal: Untrusted instruction authority · 9% estimated likelihood.
Evidence: No direct hunk selected.
Coverage: complete supplied coverage; 18/18 hunks, 7/7 files.

All estimates and evidence
Estimate Likelihood / value Direct evidence
SQL injection 3.0% No direct hunk selected
Command injection 3.0% No direct hunk selected
Weakened authentication 4.0% No direct hunk selected
Weakened authorization 8.0% No direct hunk selected
Contract regression 11.0% No direct hunk selected
Data loss 6.0% No direct hunk selected
Sensitive data disclosure 10.0% No direct hunk selected
Unexpected data transfer 5.0% No direct hunk selected
Credential misuse 6.0% No direct hunk selected
Untrusted instruction authority 9.0% No direct hunk selected
Package source redirection 4.0% No direct hunk selected
Unverified remote execution 2.0% No direct hunk selected
Privileged environment access 3.0% No direct hunk selected
Security assessment bypass 4.0% No direct hunk selected
Prohibited workload 2.0% No direct hunk selected
Primary concern Untrusted instruction authority; confidence 52.0% No direct hunk selected

Co-authored-by: openhands <openhands@all-hands.dev>
@github-actions

github-actions Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor
  ✅ **PR Artifacts Cleaned Up**

  The `.pr/` directory is no longer present.

@github-actions

github-actions Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result: ✅ PASSED

Action log

@github-actions

github-actions Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result: ✅ PASSED

Action log

@DevinVinson DevinVinson added integration-test Runs the integration tests and comments the results release-note-required PR requires explicit release-note coverage for behavioral or default changes labels Aug 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Hi! I started running the integration tests on your PR. You will receive a comment with the results shortly.

@github-actions

github-actions Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-agent-server/openhands/agent_server
   conversation_service.py114612689%181–182, 191, 218–219, 223–224, 229, 332–333, 336–337, 349–350, 364, 537–538, 599, 663, 685, 692–693, 776, 852, 903–904, 911, 943–944, 960, 991, 995, 1012, 1024–1027, 1033–1034, 1043, 1045, 1100, 1106–1107, 1111–1112, 1120, 1147, 1153, 1247, 1253, 1258, 1264, 1272–1273, 1282–1285, 1294, 1306, 1314, 1343, 1349–1350, 1353–1355, 1382, 1434, 1527–1528, 1599, 1654–1656, 1658–1659, 1662–1663, 1683, 1769–1770, 1801–1803, 1806–1807, 1811–1813, 1816–1817, 1821–1823, 1826–1827, 1856, 1865, 1908, 1918–1920, 1980, 1983, 2010, 2020, 2025–2028, 2042, 2053, 2065–2066, 2098, 2192, 2249, 2307, 2322–2323, 2463, 2709, 2762, 2765
openhands-sdk/openhands/sdk/conversation
   request.py931089%75, 334, 340, 346, 348–349, 352–353, 364, 373
   title_utils.py69297%39, 92
TOTAL41392757582% 

@github-actions

Copy link
Copy Markdown
Contributor

🧪 Integration Tests Results

Overall Success Rate: 97.7%
Total Cost: $1.87
Models Tested: 5
Timestamp: 2026-08-20 23:36:56 UTC

📁 Detailed Logs & Artifacts

Click the links below to access detailed agent/LLM logs showing the complete reasoning process for each model. On the GitHub Actions page, scroll down to the 'Artifacts' section to download the logs.

📊 Summary

Model Overall Tests Passed Skipped Total Cost Tokens
litellm_proxy_deepseek_deepseek_v4_flash 100.0% 8/8 1 9 $0.01 466,614
litellm_proxy_anthropic_claude_sonnet_4_6 88.9% 8/9 0 9 $0.53 361,344
litellm_proxy_gemini_3.1_pro_preview 100.0% 9/9 0 9 $0.50 322,192
litellm_proxy_openai_gpt_5.5 100.0% 9/9 0 9 $0.82 315,239
litellm_proxy_minimax_MiniMax_M2.7 100.0% 8/8 1 9 $0.00 252,113

📋 Detailed Results

litellm_proxy_deepseek_deepseek_v4_flash

  • Success Rate: 100.0% (8/8)
  • Total Cost: $0.01
  • Token Usage: prompt: 461,649, completion: 4,965, cache_read: 395,080, reasoning: 1,378
  • Run Suffix: litellm_proxy_deepseek_deepseek_v4_flash_37495c5_deepseek_v4_flash_run_N9_20260820_233442
  • Skipped Tests: 1

Skipped Tests:

  • t08_image_file_viewing: This test requires a vision-capable LLM model. Please use a model that supports image input.

litellm_proxy_anthropic_claude_sonnet_4_6

  • Success Rate: 88.9% (8/9)
  • Total Cost: $0.53
  • Token Usage: prompt: 356,541, completion: 4,803, cache_read: 253,704, cache_write: 102,784, reasoning: 302
  • Run Suffix: litellm_proxy_anthropic_claude_sonnet_4_6_37495c5_claude_sonnet_4_6_run_N9_20260820_233445

Failed Tests:

  • t02_add_bash_hello: Shell script is not executable (Cost: $0.06)

litellm_proxy_gemini_3.1_pro_preview

  • Success Rate: 100.0% (9/9)
  • Total Cost: $0.50
  • Token Usage: prompt: 317,552, completion: 4,640, cache_read: 108,436, reasoning: 2,684
  • Run Suffix: litellm_proxy_gemini_3.1_pro_preview_37495c5_gemini_3_1_pro_run_N9_20260820_233435

litellm_proxy_openai_gpt_5.5

  • Success Rate: 100.0% (9/9)
  • Total Cost: $0.82
  • Token Usage: prompt: 309,618, completion: 5,621, cache_read: 198,656, reasoning: 1,964
  • Run Suffix: litellm_proxy_openai_gpt_5.5_37495c5_gpt_5_5_run_N9_20260820_233444

litellm_proxy_minimax_MiniMax_M2.7

  • Success Rate: 100.0% (8/8)
  • Total Cost: $0.00
  • Token Usage: prompt: 248,534, completion: 3,579, cache_read: 178,184
  • Run Suffix: litellm_proxy_minimax_MiniMax_M2.7_37495c5_minimax_m2_7_run_N9_20260820_233435
  • Skipped Tests: 1

Skipped Tests:

  • t08_image_file_viewing: This test requires a vision-capable LLM model. Please use a model that supports image input.

@DevinVinson
DevinVinson marked this pull request as ready for review August 21, 2026 10:47
@all-hands-bot

all-hands-bot commented Aug 21, 2026 •

Copy link
Copy Markdown
Collaborator

🚦 CI is currently failing on this PR's latest commit.

Please fix the failing checks before OpenHands reviews it - this is re-checked automatically once you push a new commit. (A maintainer can also request @all-hands-bot as a reviewer to have it reviewed regardless of CI status.)

This is an automated check - no AI was used to generate this comment.

Comment thread openhands-sdk/openhands/sdk/conversation/title_utils.py Outdated
Comment on lines +99 to +116
if title_generation_prompt and title_generation_prompt.strip():
prompt = title_generation_prompt.strip()
includes_conversation = CONVERSATION_CONTENT_PLACEHOLDER in prompt
user_prompt = prompt.replace(
CONVERSATION_CONTENT_PLACEHOLDER, truncated_message
).replace(MAX_LENGTH_PLACEHOLDER, str(max_length))
if not includes_conversation:
user_prompt = f"{user_prompt}\n\nConversation content:\n{truncated_message}"
else:
user_prompt = (
f"Generate a title (maximum {max_length} characters) "
f"for a conversation that starts with this message:\n\n"
f"{truncated_message}."
"Also make sure to include ONE most relevant emoji at "
"the start of the title."
f" Choose the emoji from this list:{emojis_descriptions} "
)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

template = (title_generation_prompt or "").strip()
if template:
    user_prompt = template.replace(
        CONVERSATION_CONTENT_PLACEHOLDER, truncated_message
    ).replace(MAX_LENGTH_PLACEHOLDER, str(max_length))
    if CONVERSATION_CONTENT_PLACEHOLDER not in template:
        user_prompt += f"\n\nConversation content:\n{truncated_message}"
else:
    ...

{"emoji": "♻️", "name": "refactor", "description": "Code refactoring"},
]

CONVERSATION_CONTENT_PLACEHOLDER = "{conversation_content}"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final[str] in both from typing.

Comment thread openhands-sdk/openhands/sdk/conversation/title_utils.py Outdated
Comment thread openhands-agent-server/openhands/agent_server/conversation_service.py Outdated
Comment thread openhands-sdk/openhands/sdk/conversation/request.py Outdated
Co-authored-by: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com>
@github-actions github-actions Bot removed the release-note-required PR requires explicit release-note coverage for behavioral or default changes label Aug 21, 2026
DevinVinson and others added 4 commits August 21, 2026 09:08
Co-authored-by: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com>
…vice.py

Co-authored-by: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com>
Co-authored-by: Vasco Schiavo <115561717+VascoSch92@users.noreply.github.com>
Co-authored-by: openhands <openhands@all-hands.dev>
@DevinVinson

Copy link
Copy Markdown
Member Author

Thank you @VascoSch92 ! I've updated to prompt and hopefully addressed each of the areas you've pointed out

@all-hands-bot all-hands-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was posted by an AI agent (OpenHands).

Reviewed against head 4afe9a979fda23b125388e898d8c3f7895f2696e with repo guidance (AGENTS.md, openhands-agent-server/AGENTS.md, openhands-sdk/openhands/sdk/AGENTS.md, .agents/skills/custom-codereview-guide.md).

Scope: In scope. The change extends ConversationConfig / StoredConversation and the agent-server auto-title path, which is owned by this repo. No repository-move or product-direction gate applies.

What I verified

  • The default title prompt is reproduced byte-for-byte: the wrapper only swaps the interpolated strings for an identical user_prompt literal, and the new tests assert the default branch for None / "" / " ". Blank or absent overrides therefore keep existing behavior.
  • AutoTitleSubscriber forwards self.service.stored.prompt positionally into _generate_title_traced -> generate_title_from_message -> generate_title_with_llm; the argument order matches the new signature, and prompt is an optional keyword on the public SDK helpers, so this is backward compatible.
  • prompt is on ConversationConfig, so it round-trips through meta.json via StoredConversation (covered by test_save_meta_round_trips_title_prompt). Adding an optional field is compatible and the Persisted settings / Check OpenAPI Schema / Python-API breakage checks pass on this head.
  • Focused suite passes locally: uv run pytest tests/sdk/conversation/test_generate_title.py tests/agent_server/test_conversation_service.py -> 124 passed.

Concerns (defer to a human maintainer)

  1. Eval risk - prompt-template change. The repo's own review guide lists "prompt templates" as an eval-risk category: an APPROVE is withheld until eval evidence exists (an openhands-eval-monitor.vercel.app run plus a maintainer confirming it). This PR changes the title-generation user message and lets callers replace it entirely, which can shift auto-title output. There is no eval-monitor link or maintainer eval confirmation on the PR, and the description states no live provider-backed run was performed. This needs a human maintainer's eval sign-off before merge.

  2. CI is red on this exact head. PR Description Check (Validate PR description) and Review Thread Gate both concluded failure for 4afe9a9, and all-hands-bot already flagged the failing checks. The description check reports:

    • the first visible line must be HUMAN: and needs a short human note between HUMAN: and AGENT:; and
    • linked issue #4561 carries neither ready-for-dev nor a pre-rollout creation date (it was created 2026-08-20, after the 2026-08-13 rollout cutoff), so the linked issue must meet readiness criteria before the PR can proceed.
      The review-thread gate also reports 2 unresolved threads (the Final[str]/typing thread and the placeholder-template thread). These are process gates rather than code defects, but they currently block the PR and need the author/maintainer to address.
  3. Non-blocking design note. The field is exposed on the public /api/** contract as a bare prompt on StartConversationRequest, even though it only affects title generation. The linked issue proposed title_generation_prompt; I understand this rename came from an existing review suggestion and that the companion UI PR (OpenHands/OpenHands#16771) sends prompt, so I am not treating it as a defect - flagging only so the maintainer confirms the public field name deliberately departs from the issue's proposal and the docs PR.

No code-level correctness or security defect was found in the changed logic itself.

🔄 CHANGES REQUESTED

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration-test Runs the integration tests and comments the results

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: Allow custom title generation prompt via ConversationConfig

3 participants