OpenHands / OpenHands/software-agent-sdk
[Agent Profile] The `default` profile and named profiles build different agents — collapse launch into one pipeline
Nobody has claimed this yet.
- 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
defaultthrough theagent_settingspath. - Canvas sends
default-copythrough theagent_profile_idpath.
| 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_settingsis written by the settings pages and by LLM-profile activation. - The
defaultAgent 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_appliedis alwaysFalse). - 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.
agent_settingsbecomes an agent inside a Pydantic validator (request.py#L321-L345), before the server does anything.- The server steps run only in the
agent_profile_idbranch (conversation_service.py#L1685,_resolve_agent_from_profile):- skill discovery
- browser injection
- forced streaming
secret_refsenforcementLaunchedAgentProfileprovenance- a fresh
current_datetime
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_refsis 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_timeoutfor 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.
- The Agent Profile is the only definition of an agent.
defaultis 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.
- 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
runtimeinput, so a caller that is not the runtime can pass the runtime's answer.
- It owns every field:
- One request shape for product clients.
agent_profile_id(a stored profile), oragent_profile(an inline draft of the same model, resolved by the same function; this also covers draftmaterializefor #4958).agent_settingsis 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
agentstays as the explicit low-level option for SDK users building agents in code.
- Launch-time variation is explicit and additive, and applied the same way on every path.
agent_launch_additionscarries:- the runtime-services suffix append (already exists);
- a per-launch
llm_profile_refoverride for the chat LLM picker, recorded inLaunchedAgentProfile. client_toolsstays as it is.- Additions can never widen a profile's scope.
- 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 (
materializereports it first). No client or server silently switches to a different path or to an unscoped agent. - Derived launches keep their profile. Child conversations,
/newand 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 andmaterializeall call it, and no other code in the agent-server builds a launch agent from a profile. -
materializeand 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_datetimein the system prompt is computed at launch on every path. -
agent_settingsonStartConversationRequestis deprecated (via the deprecation helper). While it is still accepted, it goes through the same pipeline via inline-profile conversion. -
StartConversationRequestaccepts an inlineagent_profiledraft, resolved by the same function. -
AgentLaunchAdditionssupports a per-launchllm_profile_refoverride, recorded inLaunchedAgentProfile; 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 thedefaultname rule, the LLM-mismatch and dangling-ref fallbacks, theuse-llm-configuredcopy, and client-side agent assembly (buildConfigured*AgentSettings,getAgentTools, bundled-skill launch injection). It sends<RUNTIME_SERVICES>viaagent_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
defaultprofile is refreshed from current globalagent_settings(whatdefaultlaunches 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_settingsbuilder mirror the resolver. This keeps two implementations of the same logic in two languages, and still leaves global settings and thedefaultprofile as two sources of truth. - Keep global
agent_settingsas 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:
- SDK: extract the pipeline.
- Move everything in
_resolve_agent_from_profileand 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_runcall it with side effects off. - Keep the current request shapes.
- Move everything in
- SDK: inline profiles and deprecation.
- Add
agent_profiletoStartConversationRequest. - Convert
agent_settingsinto an inline profile and deprecate it. - Move the
agent_settings→ agent conversion out of the Pydantic validator and into the server pipeline.
- Add
- SDK: launch additions. Add the
llm_profile_refoverride. Return structured errors for dangling references. - 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. - 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
materializeto match the launch. - #4953: sub-agents bypass profile scope.
- #4956: prompt templates.
- #4964: self-describing profile schema.
- #5030: secret scoping.
- #4533: seeded
defaultdrops the LLM key. - OpenHands/OpenHands#16205: runtime services missing on profile launches.
- OpenHands/OpenHands#16951: home readiness vs launch disagree.
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 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