OpenHands / OpenHands/software-agent-sdk
Document the invariant: every LLM request must send a system message before the first user message
Nobody has claimed this yet.
- 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
SystemPromptEventsits at index 0/1 of the event stream. - It scans a small prefix window and raises
AssertionErrorif a userMessageEventappears before theSystemPromptEvent. 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 atinit_stateas the canonical enforcement point. - Consider a note in
AGENTS.mdor the repo README so AI agents and humans both follow it.
Content to include
- The invariant: system message first, then user/assistant/tool messages.
- Canonical example:
Agent.init_state()+events_to_messages()on the main loop. - Applies to all standalone LLM calls: condensers, goal judges, pre-flight pings, security analyzers, title generation, cleanup prompts, hook prompts, toolshield analyzers, vision inspect.
- Serialization rationale: on the Responses API
system→instructions,user→input; embedding steering in a loneusermessage breaks the separation of "how" vs "what". - 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
SystemPromptEventfor 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/complexinstructions— ordering intent preserved, shape flattened.
- 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
- 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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