OpenHands / OpenHands/software-agent-sdk

Document the invariant: every LLM request must send a system message before the first user message

Open Beginner friendly
#5,150 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

documentation
Dominant language
Python
Stars
1.1k
Forks
539
Avg merge
1d 19h
Merged PRs (30d)
137

Description

Summary

Document a repository-wide pattern for how every LLM request must be constructed, so future contributors (and AI agents) don't reintroduce user-before-system message lists.

The pattern

Every LLM request built in this repository must place a system message before the first user message.

The canonical guarantee already lives in Agent.init_state() (openhands-sdk/openhands/sdk/agent/agent.py):

  • The agent loop guarantees the SystemPromptEvent sits at index 0/1 of the event stream.
  • It scans a small prefix window and raises AssertionError if a user MessageEvent appears before the SystemPromptEvent.
  • LLMConvertibleEvent.events_to_messages() (openhands-sdk/openhands/sdk/event/base.py) projects the event stream to messages in order, preserving the system lead-in.

Where to document

  • Add a section to the SDK developer guide / contributing docs (or a docs/ page under the SDK docs) titled something like "LLM message construction" that states the invariant and points at init_state as the canonical enforcement point.
  • Consider a note in AGENTS.md or the repo README so AI agents and humans both follow it.

Content to include

  1. The invariant: system message first, then user/assistant/tool messages.
  2. Canonical example: Agent.init_state() + events_to_messages() on the main loop.
  3. Applies to all standalone LLM calls: condensers, goal judges, pre-flight pings, security analyzers, title generation, cleanup prompts, hook prompts, toolshield analyzers, vision inspect.
  4. Serialization rationale: on the Responses API systeminstructions, userinput; embedding steering in a lone user message breaks the separation of "how" vs "what".
  5. Known exceptions (documented deliberately):
    • ACP agents: the real system prompt/tools are managed by the ACP server, not by an SDK-constructed message list; the SDK emits a placeholder SystemPromptEvent for the visualizer.
    • Subscription/Codex transport (transform_for_subscription): system chunks are prepended into the first user message's content because Codex-style endpoints reject long/complex instructions — ordering intent preserved, shape flattened.
  6. A checklist for reviewers: "First message role must be system, or there must be a documented exception."

Related

  • Parent tracking issue: #5144 (system before first user invariant).
  • #5142 — condenser summarization split into system + user (fix example already merged into PR #5143).

This issue was created by an AI agent (OpenHands) on behalf of juanmichelini.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with openhands-sdk/openhands/sdk/agent/agent.py and openhands-sdk/openhands/sdk/event/base.py, reading Agent.init_state() and LLMConvertibleEvent.events_to_messages(). Find the SDK developer or contributing documentation, then document the system-first invariant, its standalone-call scope, serialization rationale, exceptions, and reviewer checklist. Done means the canonical enforcement points and both named exceptions are clearly documented.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
documentation
Issue type
Documentation
Difficulty
2/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.