OpenHands / OpenHands/software-agent-sdk

[Agent Profile] The `default` profile and named profiles build different agents — collapse launch into one pipeline

Open
#5,141 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

architecture enhancement ready-for-dev
Dominant language
Python
Stars
1.1k
Forks
539
Avg merge
1d 19h
Merged PRs (30d)
137

Description

Is there an existing feature request for this?
  • I have searched existing issues and feature requests, and this is not a duplicate.

This is the root cause behind a series of already-fixed issues, each of which fixed one field on one path: #3967, #4014, #4016, #4542. It is also the root cause of open issues #4533, OpenHands/OpenHands#16205 and OpenHands/OpenHands#16951. #4958 (tool catalog and resolved tools) depends on it.

Problem or Use Case

Launching the default Agent Profile and launching a named Agent Profile build different agents from the same stored settings. The launch has several code paths, and each one sets agent fields in its own way. Every profile field has to be implemented, and kept correct, once per path.

Live evidence

Setup: agent-server 1.49.1 built from main (365ceb3), Agent Canvas main (07590fd), and the e2e mock LLM, which records every completion request.

I saved two profiles with identical contents, one named default and one named default-copy, and launched each from the canvas home page:

  • Canvas sends default through the agent_settings path.
  • Canvas sends default-copy through the agent_profile_id path.
Field set on the profile default (agent_settings path) default-copy (agent_profile_id path)
tools: [terminal, glob] ignored: terminal, file_editor, task_tracker, browser_tool_set applied
system_message_suffix ignored applied
mcp_server_refs: [] ignored: the fetch MCP server is still attached applied
disabled_skills, enable_switch_llm_tool: false, tool_concurrency_limit: 3, condenser disabled all ignored all applied
Skills in the system prompt, with nothing customised 16 (canvas's bundled set) 68 (server discovery)
"The current date and time is" 11:54:29, when settings were last saved, for a launch at 12:10 12:10
Setting secret_refs moves default onto the agent_profile_id path applied

The model sees a different agent depending on the profile's name. Profile edits made to default do nothing on the most common launch, the home page.

Why

1. Two definitions of the default agent.

  • Global agent_settings is written by the settings pages and by LLM-profile activation.
  • The default Agent Profile is copied from it once (build_seed_profile). After that it is written by the profile editor, and onboarding overwrites it with {agent_kind, llm_profile_ref}.
  • Agent-profile activation only moves the active-profile pointer (agent_settings_applied is always False).
  • Nothing keeps the two in sync.

2. Three request inputs that do different amounts of work. StartConversationRequest accepts agent, agent_settings or agent_profile_id.

3. A second copy of the server pipeline. The Docker runtime's prepare_start repeats the profile branch (resolution, secret filtering, the load_memory stamp) and can drift from conversation_service.

4. Clients pick the path, and rebuild the agent themselves on the agent_settings path.

Agent Canvas chooses the path in use-create-conversation.ts with these rules:

  • The profile is named default.
  • The profile's LLM reference is missing, or the LLM list fails to load.
  • The active LLM differs from the profile's pinned LLM.
  • A veto when secret_refs is set.

The same rule is copied in use-llm-configured.ts, and the copies already disagree (OpenHands/OpenHands#16951).

Launches that ignore the profile entirely:

  • /new
  • locally launched child conversations
  • the local planner, which sends a hand-built agent

On the agent_settings path, canvas assembles the agent itself: buildConfiguredOpenHandsAgentSettings, getAgentTools, buildBundledSkills/buildAgentContext. It uses:

  • the account-wide active LLM instead of llm_profile_ref;
  • a bundled skill catalog filtered by a global allow-list, instead of server discovery minus disabled_skills;
  • a <RUNTIME_SERVICES> block that replaces the stored suffix;
  • its own tool list, gated by a Vite flag;
  • global condenser, verification (including critic_api_key), sub-agents, switch-LLM and concurrency settings;
  • the whole MCP config, unfiltered;
  • secrets with no scoping;
  • no acp_startup_timeout for ACP.

Canvas never sends agent_launch_additions, the field that was added to carry <RUNTIME_SERVICES> on the profile path (#4029).

5. Cloud has its own version of the problem. The cloud app server resolves the profile itself and sends a fully built agent, so the agent-server's profile path never runs there. It then:

  • overwrites several fields the profile carries (tools, system_message_suffix, the switch-LLM off-switch);
  • always adds its platform MCP server;
  • launches without the profile when resolution fails, with nothing surfaced to the user;
  • launches sub-conversations without the parent's profile.
Cost

Every new profile field (tools, secret_refs, prompt templates in #4956, tool metadata in #4958) has to be implemented once per path, and each path is a new place for a silent mismatch. The issues listed at the top are that cost already paid. For a scoped agent it is also a correctness problem: a restriction on the profile only holds on the launch paths that honour it.

Desired Behavior

One launch pipeline. Every conversation start goes through the same resolution: default or named profile, home page or /new, child conversation, automation, local or Docker runtime, cloud. The only inputs that vary are which profile and an explicit, additive set of launch-time additions.

  1. The Agent Profile is the only definition of an agent.
    • default is an ordinary profile: no name-based rules anywhere.
    • Global settings keep only the shared pieces profiles refer to: LLM profiles, the MCP server list, secrets, the skill catalog and marketplaces.
    • Behaviour settings (condenser, verification, memory, sub-agents, switch-LLM, concurrency, tools, instructions, skill enablement) live on the profile. Settings pages that edit them edit the active profile.
  2. One SDK function builds the launch. Something like prepare_agent_launch(profile, *, stores, mcp_catalog, skill_catalog, runtime, additions) -> LaunchPlan(agent, launched_profile, allowed_secrets, resolved_view).
    • It owns every field:
      • LLM and streaming
      • MCP filtering
      • skill catalog minus disabled_skills
      • project-skill loading
      • suffix plus additions
      • current_datetime (always computed at launch)
      • load_memory
      • tool defaults and runtime-dependent injections
      • sub-agents, switch-LLM, concurrency, condenser, verification
      • secret scope
      • ACP fields and ACP skill sourcing
    • Callers:
      • conversation_service
      • Docker mediation (its duplicate branch is deleted)
      • materialize, as a side-effect-free call of the same function, so diagnostics can never disagree with the launch
      • the cloud app server
    • Runtime-dependent decisions (e.g. whether browser is available) come in as an explicit runtime input, so a caller that is not the runtime can pass the runtime's answer.
  3. One request shape for product clients.
    • agent_profile_id (a stored profile), or agent_profile (an inline draft of the same model, resolved by the same function; this also covers draft materialize for #4958).
    • agent_settings is deprecated. During the deprecation window the server converts it into an inline profile, so it takes the same pipeline instead of a validator shortcut.
    • Raw agent stays as the explicit low-level option for SDK users building agents in code.
  4. Launch-time variation is explicit and additive, and applied the same way on every path. agent_launch_additions carries:
    • the runtime-services suffix append (already exists);
    • a per-launch llm_profile_ref override for the chat LLM picker, recorded in LaunchedAgentProfile.
    • client_tools stays as it is.
    • Additions can never widen a profile's scope.
  5. Failures are explicit. A dangling LLM or MCP reference fails the launch with a structured error that clients can turn into a fix-it prompt (materialize reports it first). No client or server silently switches to a different path or to an unscoped agent.
  6. Derived launches keep their profile. Child conversations, /new and sub-conversations launch with the parent's launched profile unless a different one is chosen explicitly.
Acceptance Criteria
  • A single SDK function builds the agent for every launch. conversation_service, Docker mediation and materialize all call it, and no other code in the agent-server builds a launch agent from a profile.
  • materialize and a real launch produce the same resolved agent for the same profile. A test compares them field by field: tools, MCP keys, skills, suffix, LLM, condenser, verification, sub-agents, switch-LLM, concurrency, secret scope.
  • Two profiles with identical contents and different names (one of them default) produce identical agents, apart from launch timestamps and ids, on every product launch path. A cross-repo e2e test reproduces the table above and asserts every row matches.
  • current_datetime in the system prompt is computed at launch on every path.
  • agent_settings on StartConversationRequest is deprecated (via the deprecation helper). While it is still accepted, it goes through the same pipeline via inline-profile conversion.
  • StartConversationRequest accepts an inline agent_profile draft, resolved by the same function.
  • AgentLaunchAdditions supports a per-launch llm_profile_ref override, recorded in LaunchedAgentProfile; additions cannot widen scope.
  • A dangling LLM or MCP reference returns a structured error from the start endpoint, with no silent fallback.
  • Follow-up in OpenHands/OpenHands: canvas always launches with a profile (active, explicit, or the parent's for children and /new). It deletes the default name rule, the LLM-mismatch and dangling-ref fallbacks, the use-llm-configured copy, and client-side agent assembly (buildConfigured*AgentSettings, getAgentTools, bundled-skill launch injection). It sends <RUNTIME_SERVICES> via agent_launch_additions.
  • Follow-up for cloud: resolve through the same SDK function, stop overwriting fields the profile carries, and surface resolution failures instead of launching without the profile.
  • One-time migration: the default profile is refreshed from current global agent_settings (what default launches actually ran), and the settings pages edit the active profile from then on.
Alternatives Considered
  • Keep both paths and fix fields one by one. This is what #3967, #4014, #4016, #4029 and #4542 did. Each fix closed one gap on one path, and the table above shows the paths still differ on nearly every field. Every new profile field restarts the cycle.
  • Make canvas's agent_settings builder mirror the resolver. This keeps two implementations of the same logic in two languages, and still leaves global settings and the default profile as two sources of truth.
  • Keep global agent_settings as the default and treat profiles as overlays. Profiles already store concrete values rather than deltas, and the design settled in OpenHands/OpenHands#15563 puts behaviour settings on the profile only. Overlays would add a third composition model.
Priority / Severity

High - Significant impact on productivity

Estimated Scope

Large - Significant feature requiring architecture changes

Feature Area
  • Agent API / Core functionality
  • Tools / Tool system
  • Skills / Plugins
  • Agent Server
  • Workspace management
  • Configuration / Settings
  • Examples / Templates
  • Documentation
  • Testing / Development tools
  • Performance / Optimization
  • Integrations (GitHub, APIs, etc.)
  • Other
Technical Implementation Ideas (Optional)

Suggested order, each step shippable on its own:

  1. SDK: extract the pipeline.
    • Move everything in _resolve_agent_from_profile and the profile branch of _start_conversation, plus the Docker mediation copy, into one SDK function that takes explicit stores, catalogs and runtime inputs.
    • Make resolve_agent_profile_dry_run call it with side effects off.
    • Keep the current request shapes.
  2. SDK: inline profiles and deprecation.
    • Add agent_profile to StartConversationRequest.
    • Convert agent_settings into an inline profile and deprecate it.
    • Move the agent_settings → agent conversion out of the Pydantic validator and into the server pipeline.
  3. SDK: launch additions. Add the llm_profile_ref override. Return structured errors for dangling references.
  4. Canvas. Send a profile on every launch path, delete the routing rules and client-side agent assembly, move the behaviour settings pages onto the active profile, and migrate the global skill toggles into the profile's disabled_skills.
  5. Cloud. Adopt the SDK function and remove the field overwrites.

Decisions to settle before step 4:

  • Skill model. Canvas has a global allow-list (enabled_skills); the SDK profile has a deny-list (disabled_skills). Proposal: keep the per-profile deny-list and migrate the global toggles into it.
  • Chat LLM picker. Per-launch override (proposed) or an edit to the profile.
  • Tools that ignore profile scope. The cloud platform MCP server and plugin-provided MCP servers are currently added outside mcp_server_refs. They need an explicit policy inside the function: exempt from profile scoping, or subject to it.
  • Cloud fallback policy. Cloud currently launches without the profile when resolution fails so that no launch is ever blocked. Keep that with a visible warning, or fail the launch.
Additional Context

Related:

  • #4958: tool catalog and resolved tools; needs this for materialize to match the launch.
  • #4953: sub-agents bypass profile scope.
  • #4956: prompt templates.
  • #4964: self-describing profile schema.
  • #5030: secret scoping.
  • #4533: seeded default drops the LLM key.
  • OpenHands/OpenHands#16205: runtime services missing on profile launches.
  • OpenHands/OpenHands#16951: home readiness vs launch disagree.

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 StartConversationRequest in openhands-sdk/openhands/sdk/conversation/request.py, then trace profile resolution in conversation_service.py and Docker mediation's prepare_start. Compare those paths with the client launch logic in use-create-conversation.ts and agent-server-adapter.ts. Done means one shared launch function handles every listed path and the acceptance tests show identical resolved agents for equivalent profiles.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, typescript
Domain
backend, backend-api-design, full-stack
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.