Skip to content

refactor(sdk): build dynamic system-message suffix via the prompt registry#3837

Merged
VascoSch92 merged 2 commits into
mainfrom
vasco/migrate-acp-suffix-to-registry
Jun 26, 2026
Merged

refactor(sdk): build dynamic system-message suffix via the prompt registry#3837
VascoSch92 merged 2 commits into
mainfrom
vasco/migrate-acp-suffix-to-registry

Conversation

@VascoSch92

@VascoSch92 VascoSch92 commented Jun 22, 2026

Copy link
Copy Markdown
Member

HUMAN:

Deleting the dynamic system promtp jinja.


AGENT:

Why

system_message_suffix.j2 was the last prompt template still rendered through the
Jinja path, even though its content had already been ported verbatim into the typed
prompt registry's dynamic-tier sections (context/prompts/sections/dynamic.py,
#3610). That left two sources of truth for the same suffix, kept in sync only by
snapshot tests. The main Agent already builds its dynamic block from the registry
(AgentBase.dynamic_context); only AgentContext.get_system_message_suffix — and the
ACP path that delegates to it — still rendered the template.

Summary

  • AgentContext.get_system_message_suffix now assembles the dynamic-tier sections via create_registry().build(ctx).dynamic instead of rendering system_message_suffix.j2. Public signature unchanged (no API break).
  • Deleted openhands-sdk/openhands/sdk/context/prompts/templates/system_message_suffix.j2 — the duplicate source of truth.
  • Removed stale provenance comments that pointed at the deleted template.

Issue Number

Follow-up to the prompt-registry port (#3610).

How to Test

Run the test suites that cover the suffix/registry/ACP paths:

uv run pytest tests/sdk/agent tests/sdk/context
# 1162 passed

End-to-end render through the migrated path (not just unit tests) — build a context
with a repo skill, a triggered skill, a custom suffix, secrets, and a datetime:

from openhands.sdk.context.agent_context import AgentContext
from openhands.sdk.skills import Skill
from openhands.sdk.skills.skill import KeywordTrigger
from pydantic import SecretStr
from openhands.sdk.secret import StaticSecret

ctx = AgentContext(
    skills=[
        Skill(name="repo-rules", content="Follow PEP 8.", trigger=None),
        Skill(name="pdf", content="Extract PDFs.", description="PDF tools.",
              trigger=KeywordTrigger(keywords=["pdf"])),
    ],
    system_message_suffix="Be concise.",
    secrets={"GITHUB_TOKEN": StaticSecret(value=SecretStr("x"), description="GH token")},
    current_datetime="2026-06-22T10:00:00+00:00",
)
print(ctx.get_system_message_suffix())
print("ACP delegates to same path:",
      ctx.to_acp_prompt_context() == ctx.get_system_message_suffix())

Result: renders <CURRENT_DATETIME>, <REPO_CONTEXT>, <SKILLS>, the custom suffix,
and <CUSTOM_SECRETS> (advertising $GITHUB_TOKEN); to_acp_prompt_context() returns
the identical string.

Behavior-preservation vs main — rendering the same context on main (the .j2
renderer) and on this branch (the registry) and diffing:

  • The output is content-identical after collapsing blank-line runs (True).
  • The only byte difference is inter-section spacing: the registry joins sections with a
    single blank line, while the old template's untrimmed {% if %} blocks left 2-5. This
    matches what AgentBase.dynamic_context already emitted, and the existing parity
    tests normalize it (_canonical_gaps).

No public API breakgriffe.find_breaking_changes(main, HEAD) for openhands.sdk
reports 0 breaking changes (removed public class members: NONE), so the
api-breakage gate stays green.

Video/Screenshots

N/A — internal prompt-assembly refactor with no UI surface. Rendered-output evidence is
under "How to Test".

Type

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

Notes

  • One residual: a comment in base.py still says get_system_message_suffix "renders"
    the string — left as-is to keep the diff minimal now that the method builds via the
    registry; happy to tweak that one line on request.

Agent Server images for this PR

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:8d135e0-python

Run

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

All tags pushed for this build

ghcr.io/openhands/agent-server:8d135e0-golang-amd64
ghcr.io/openhands/agent-server:8d135e072d43eb52e702c22e43e0c76cb7f312a6-golang-amd64
ghcr.io/openhands/agent-server:vasco-migrate-acp-suffix-to-registry-golang-amd64
ghcr.io/openhands/agent-server:8d135e0-golang_tag_1.21-bookworm-amd64
ghcr.io/openhands/agent-server:8d135e0-golang-arm64
ghcr.io/openhands/agent-server:8d135e072d43eb52e702c22e43e0c76cb7f312a6-golang-arm64
ghcr.io/openhands/agent-server:vasco-migrate-acp-suffix-to-registry-golang-arm64
ghcr.io/openhands/agent-server:8d135e0-golang_tag_1.21-bookworm-arm64
ghcr.io/openhands/agent-server:8d135e0-java-amd64
ghcr.io/openhands/agent-server:8d135e072d43eb52e702c22e43e0c76cb7f312a6-java-amd64
ghcr.io/openhands/agent-server:vasco-migrate-acp-suffix-to-registry-java-amd64
ghcr.io/openhands/agent-server:8d135e0-eclipse-temurin_tag_17-jdk-amd64
ghcr.io/openhands/agent-server:8d135e0-java-arm64
ghcr.io/openhands/agent-server:8d135e072d43eb52e702c22e43e0c76cb7f312a6-java-arm64
ghcr.io/openhands/agent-server:vasco-migrate-acp-suffix-to-registry-java-arm64
ghcr.io/openhands/agent-server:8d135e0-eclipse-temurin_tag_17-jdk-arm64
ghcr.io/openhands/agent-server:8d135e0-python-amd64
ghcr.io/openhands/agent-server:8d135e072d43eb52e702c22e43e0c76cb7f312a6-python-amd64
ghcr.io/openhands/agent-server:vasco-migrate-acp-suffix-to-registry-python-amd64
ghcr.io/openhands/agent-server:8d135e0-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-amd64
ghcr.io/openhands/agent-server:8d135e0-python-arm64
ghcr.io/openhands/agent-server:8d135e072d43eb52e702c22e43e0c76cb7f312a6-python-arm64
ghcr.io/openhands/agent-server:vasco-migrate-acp-suffix-to-registry-python-arm64
ghcr.io/openhands/agent-server:8d135e0-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim-arm64
ghcr.io/openhands/agent-server:8d135e0-golang
ghcr.io/openhands/agent-server:8d135e072d43eb52e702c22e43e0c76cb7f312a6-golang
ghcr.io/openhands/agent-server:vasco-migrate-acp-suffix-to-registry-golang
ghcr.io/openhands/agent-server:8d135e0-golang_tag_1.21-bookworm
ghcr.io/openhands/agent-server:8d135e0-java
ghcr.io/openhands/agent-server:8d135e072d43eb52e702c22e43e0c76cb7f312a6-java
ghcr.io/openhands/agent-server:vasco-migrate-acp-suffix-to-registry-java
ghcr.io/openhands/agent-server:8d135e0-eclipse-temurin_tag_17-jdk
ghcr.io/openhands/agent-server:8d135e0-python
ghcr.io/openhands/agent-server:8d135e072d43eb52e702c22e43e0c76cb7f312a6-python
ghcr.io/openhands/agent-server:vasco-migrate-acp-suffix-to-registry-python
ghcr.io/openhands/agent-server:8d135e0-nikolaik_s_python-nodejs_tag_python3.13-nodejs22-slim

About Multi-Architecture Support

  • Each variant tag (e.g., 8d135e0-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., 8d135e0-python-amd64) are also available if needed

…istry

get_system_message_suffix now assembles the dynamic-tier sections through
create_registry().build(ctx).dynamic instead of rendering the
system_message_suffix.j2 template, which is deleted. The template's content
was already ported verbatim into the registry's dynamic sections (#3610), so
this removes the duplicate source of truth without changing the public API.
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Python API breakage checks — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

REST API breakage checks (OpenAPI) — ✅ PASSED

Result:PASSED

Action log

@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Coverage

Coverage Report •
FileStmtsMissCoverMissing
openhands-sdk/openhands/sdk/context
   agent_context.py162596%412–413, 483, 506, 512
TOTAL33087916372% 

@VascoSch92 VascoSch92 requested a review from all-hands-bot June 22, 2026 13:46

all-hands-bot commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Review complete.

This review was performed through OpenHands Cloud Automation. You can log in and view the conversation here.

@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.

🟢 Good taste — well-scoped refactor that consolidates dynamic-tier rendering into the existing section registry without changing observable behavior.

Summary. The PR deletes context/prompts/templates/system_message_suffix.j2 and rewrites AgentContext.get_system_message_suffix (and the ACP path it now shares) to build the dynamic block via create_registry().build(ctx).dynamic. The five dynamic sections (DateTimeSection, RepoContextSection, AvailableSkillsSection, CustomSuffixSection, CustomSecretsSection) already exist in sections/dynamic.py; the change wires them into the same factory that powers the static block.

Equivalence check. Walked each section against the deleted .j2:

  • Order: DateTime → RepoContext → AvailableSkills → CustomSuffix → CustomSecrets — preserved.
  • Guards: now / repo_skills / available_skills_prompt / custom_suffix.strip() / secret_infos truthiness — preserved.
  • Custom-suffix wrapping: Old template wrapped the user value with leading/trailing blank lines; new code returns ctx.custom_suffix raw and the registry joins/strips at the outer level. Net output is byte-identical after the outer .strip().
  • Empty-output return None: when every section is guarded off, dynamic is None — matches the old return None branch.
  • ACP path: to_acp_prompt_context now delegates to get_system_message_suffix, eliminating the last duplicate render path — strictly an improvement.

Tests. No test logic changed, only a docstring in test_acp_agent.py. That's fine because coverage is real:

  • tests/sdk/context/prompts/test_prompt_snapshot.py pins the rendered dynamic block as golden files across the full model-family × browser × security-analyzer × cli-mode matrix, so any whitespace drift will fail.
  • tests/sdk/context/test_agent_context.py exercises each section (datetime-only, repo skills, available skills, custom suffix alone, secrets alone, and combinations).
  • tests/sdk/context/test_agent_context_model_specific.py covers the model-family gating preserved from the old template.

Risk. 🟢 LOW. Pure refactor; net source delta is −85/+25 lines; no public API change; no behavior change for callers.

Approving.

This review was generated by an AI agent (OpenHands) on behalf of the user through OpenHands Automation. View conversation

@VascoSch92 VascoSch92 requested a review from enyst June 22, 2026 13:54
@VascoSch92 VascoSch92 marked this pull request as ready for review June 22, 2026 13:54

@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.

✅ QA Report: PASS

Verified the SDK prompt-suffix path with real AgentContext calls on main and this PR; the PR preserves rendered dynamic context behavior while ACP continues delegating to the same output.

Does this PR achieve its stated goal?

Yes. The PR set out to remove the duplicated Jinja dynamic system-message suffix path and route AgentContext.get_system_message_suffix() / ACP prompt context through the registry without changing public behavior. Running the SDK against main and then commit 7ec9a845 showed the rich dynamic suffix still includes datetime, repo context, available skills, custom suffix, and custom secrets; to_acp_prompt_context() still returns the identical string; and canonicalized output matched the baseline exactly (d68ebaf1756697fc0a50e2d63832e132afc3aeef8401d5fa8f08853ab87ca36a).

Phase Result
Environment Setup make build completed successfully and installed the uv-managed dev environment.
CI Status 🟡 GitHub reported 35 passing checks, 0 failing checks, 1 pending check (qa-changes), and 2 skipped checks at review time.
Functional Verification ✅ Real SDK function calls matched baseline behavior; ACP delegation and no-content edge cases were verified.
Functional Verification

Test 1: Rich dynamic system-message suffix and ACP delegation

Step 1 — Establish baseline without the fix:
Ran git checkout origin/main && uv run python /tmp/qa_suffix_check.py using a real AgentContext containing a repo skill, keyword-triggered skill, custom suffix, static secret, additional secret info, and fixed datetime. Relevant output:

suffix_is_none False
acp_matches_suffix True
missing_fragments []
canonical_sha256 d68ebaf1756697fc0a50e2d63832e132afc3aeef8401d5fa8f08853ab87ca36a
canonical_line_count 51
raw_line_count 57

This shows the pre-PR SDK rendered all expected dynamic sections and ACP delegated to the same suffix output.

Step 2 — Apply the PR's changes:
Checked out 7ec9a845cac99b89bcf26c3ee46a31ad7f54aa82 (refactor(sdk): build dynamic system-message suffix via the prompt registry).

Step 3 — Re-run with the fix in place:
Ran uv run python /tmp/qa_suffix_check.py again. Relevant output:

suffix_is_none False
acp_matches_suffix True
missing_fragments []
canonical_sha256 d68ebaf1756697fc0a50e2d63832e132afc3aeef8401d5fa8f08853ab87ca36a
canonical_line_count 51
raw_line_count 52

This confirms the registry-backed path still renders the required user-visible prompt sections and ACP still receives identical prompt context. The canonical hash matches main; the raw line-count reduction reflects the PR-described blank-line normalization.

Test 2: No dynamic content and custom-only edge behavior

Step 1 — Establish baseline without the fix:
Ran git checkout origin/main && OPENHANDS_SUPPRESS_BANNER=1 uv run python /tmp/qa_suffix_edge_check.py:

no_dynamic_content None
custom_no_datetime 'Custom only.'

This confirms the old path returned None when datetime and all dynamic inputs were absent, and trimmed a custom-only suffix.

Step 2 — Apply the PR's changes:
Checked out 7ec9a845cac99b89bcf26c3ee46a31ad7f54aa82.

Step 3 — Re-run with the fix in place:
Ran OPENHANDS_SUPPRESS_BANNER=1 uv run python /tmp/qa_suffix_edge_check.py:

no_dynamic_content None
custom_no_datetime 'Custom only.'

This confirms the no-content and custom-only edge behavior remains unchanged after the registry migration.

Issues Found

None.

This review was created by an AI agent (OpenHands) on behalf of the user.

@VascoSch92

Copy link
Copy Markdown
Member Author

@enyst wdyt?

@VascoSch92 VascoSch92 enabled auto-merge (squash) June 26, 2026 06:45
@VascoSch92 VascoSch92 merged commit aa0494b into main Jun 26, 2026
29 checks passed
@VascoSch92 VascoSch92 deleted the vasco/migrate-acp-suffix-to-registry branch June 26, 2026 06:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants