OpenHands / OpenHands/software-agent-sdk

[Agent Profile] Profiles can't select a system prompt template, only append to one

Open
#4,956 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

architecture enhancement security-related
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:

  1. A named-template enum — the profile selects from templates the server knows (default, planning, …), never a path. Closes the traversal vector entirely; least expressive.
  2. 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).
  3. Inline system_prompt behind 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 AgentProfile can select a non-default system prompt template, and resolve_agent_profile() carries it onto the resolved AgentSettings.
  • create_agent() builds an agent with that template, so a profile launch and a direct Agent(...) 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_kwargs reach 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_path is 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.pysystem_prompt, system_prompt_filename, system_prompt_kwargs
  • openhands-sdk/openhands/sdk/settings/model.pyAgentSettings / create_agent(), where they are absent
  • openhands-sdk/openhands/sdk/profiles/agent_profile.pyOpenHandsAgentProfile
  • openhands-tools/openhands/tools/preset/planning.pyget_planning_agent(), the motivating construction
  • Cloud workaround: _apply_server_only_overrides in live_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

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/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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.