OpenHands / OpenHands/software-agent-sdk
[Agent Profile] Profiles can't select a system prompt template, only append to one
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 1.1k
- Forks
- 539
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 137
Description
An AgentProfile's "custom prompt" is system_message_suffix — text appended to the standard system prompt. A purpose-driven agent sometimes needs a different prompt, not an appended one, and the SDK's own planning preset is the proof:
# openhands-tools/openhands/tools/preset/planning.py
agent = Agent(
llm=llm,
tools=get_planning_tools(),
system_prompt_filename='system_prompt_planning.j2',
system_prompt_kwargs={'plan_structure': format_plan_structure()},
condenser=get_planning_condenser(...),
)
That is a profile in all but name — tools, prompt, condenser — except the prompt half cannot be expressed as one.
The gap
AgentBase has three prompt fields:
| Field | Default | Purpose |
|---|---|---|
system_prompt_filename |
"system_prompt.j2" |
template file; relative to the prompts dir, or an absolute path |
system_prompt_kwargs |
{} |
variables rendered into that template |
system_prompt |
None |
inline verbatim override of the whole system message |
None is reachable from a profile launch. AgentSettings does not expose them (grep system_prompt_filename settings/model.py → nothing), so create_agent() cannot build a planning-style agent, and neither can resolve_agent_profile().
Cloud works around it in _apply_server_only_overrides, whose docstring says the quiet part out loud:
Apply server-only fields that have no place in
AgentSettings. System-prompt filename / kwargs (planning vs default agent).
So the capability exists, is used in production for the planning agent, and is reachable only by hardcoding a branch on AgentType.PLAN in the serving layer.
Why it matters now
This is the missing half of OSS-10492's first goal — "a Code Exploration Agent that has a custom prompt". OpenHands/OpenHands#17235 delivers the suffix, which covers "add instructions to the standard agent" but not "this agent reasons differently". Competitor equivalents (Devin's playbooks, Factory's droid prompts, Copilot's custom agents) all own the whole prompt, not a suffix.
Security: do not expose the raw fields
system_prompt_filename accepts an absolute path, and profiles are user-authored data synced from a client. Exposing the field as-is would let a saved profile name an arbitrary file on the runtime, which is then rendered into the system prompt and returned to the model — an arbitrary-file-read vector, worse on a multi-tenant deployment where profiles live in an org store.
system_prompt (inline verbatim) is less dangerous but carries the SDK's own warning that it "will override OpenHands' built-in system instructions that govern default agent behaviour" — including the security policy framing.
A constrained surface is the safer shape. Options worth weighing:
- A named-template enum — the profile selects from templates the server knows (
default,planning, …), never a path. Closes the traversal vector entirely; least expressive. - A path confined to the prompts directory, rejecting absolute paths and
... More expressive, and the validation has to hold on every path into the field (profile save, settings apply, direct API). - Inline
system_promptbehind a deployment flag, off by default, so self-hosted users who want it can opt in and cloud does not.
I would start with (1): it covers planning-as-profile, is trivially safe, and does not foreclose (2).
Runtime-computed kwargs
Planning's system_prompt_kwargs={'plan_structure': format_plan_structure()} is computed at launch, and its plan_path tool param is derived from the workspace and the git provider (_compute_plan_path). A static profile cannot hold either.
This is the same shape as the existing browser-tool injection, where the serving layer supplies what only it knows. Whatever is designed here should say explicitly how a profile-named template gets its runtime kwargs — otherwise the field looks usable and quietly renders an incomplete prompt.
Acceptance Criteria
- A decision is recorded on the exposed surface (named enum / confined path / flagged inline).
- An
AgentProfilecan select a non-default system prompt template, andresolve_agent_profile()carries it onto the resolvedAgentSettings. -
create_agent()builds an agent with that template, so a profile launch and a directAgent(...)construction agree. - A profile cannot cause an arbitrary filesystem path to be read into the system prompt; the validation is enforced server-side, not only in the editor.
- A profile that sets nothing keeps today's prompt byte-identical.
- How runtime-computed
system_prompt_kwargsreach a profile-selected template is specified.
Follow-on: planning as a profile
With this field, the planning agent becomes 4/5 expressible as a seeded built-in profile (tools, prompt template, condenser, read-only tool scope). Two things still block the full collapse of the AgentType.PLAN branches, out of scope here:
plan_pathis computed per launch from workspace + git provider.- Plan ↔ code switching is mid-conversation (the Build button, Shift+Tab), while agent profiles are selected at conversation start.
Worth noting the current layering is already slightly incoherent: cloud prepends PLANNING_AGENT_INSTRUCTION ("you CANNOT execute code or make changes") to the suffix, so a planning conversation launched from a profile that says otherwise gets two prompt authorities with no defined precedence. Planning-as-profile would give it one.
Relevant code
openhands-sdk/openhands/sdk/agent/base.py—system_prompt,system_prompt_filename,system_prompt_kwargsopenhands-sdk/openhands/sdk/settings/model.py—AgentSettings/create_agent(), where they are absentopenhands-sdk/openhands/sdk/profiles/agent_profile.py—OpenHandsAgentProfileopenhands-tools/openhands/tools/preset/planning.py—get_planning_agent(), the motivating construction- Cloud workaround:
_apply_server_only_overridesinlive_status_app_conversation_service.py(OpenHands/enterprise) - Related: OpenHands/software-agent-sdk#4953 (sub-agent delegation escapes profile scope), OpenHands/OpenHands#17235 (profile suffix + tools + MCP)
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/settings/model.py and profiles/agent_profile.py, then compare their absent profile fields with AgentBase in openhands-sdk/openhands/sdk/agent/base.py and get_planning_agent() in openhands-tools/openhands/tools/preset/planning.py. Trace resolve_agent_profile() and create_agent(), and use the acceptance criteria to define a safe exposed surface, runtime kwargs behavior, and unchanged default prompting.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- ai, backend-api-design
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100