OpenHands / OpenHands/software-agent-sdk
[Agent Profile] Clients can't ask what tools an agent will get, or which tools are pickable
@simonrosenberg is already working on this.
Since Sep 17, 2026.
- Dominant language
- Python
- Stars
- 1.1k
- Forks
- 539
- Avg merge
- 1d 19h
- Merged PRs (30d)
- 137
Description
A client building an Agent Profile editor has to answer two questions about tools, and the SDK can answer neither:
- What will this profile's agent actually get?
- Which tools may a user pick from?
Both are facts the SDK owns, so every client ends up guessing — OpenHands/OpenHands#17235 currently hardcodes a six-name allow-list and a copy of DEFAULT_EXEC_TOOL_NAMES to fill the gap.
1. materialize reports intent, not outcome
POST /api/agent-profiles/{name}/materialize runs resolve_agent_profile_dry_run and is otherwise the right endpoint for this. But _build_openhands_settings passes the field straight through ("tools": profile.tools), so a profile that leaves tools unset materializes as None — while the launch resolves it to four tools.
Verified against a live agent-server (1.46.0), same profile:
POST /api/agent-profiles/probe-default/materialize
resolved_settings.tools : None
POST /api/conversations {agent_profile_id: <that profile>}
agent.tools : ['browser_tool_set', 'file_editor', 'task_tracker', 'terminal']
The gap is not just create_agent()'s defaulting. _resolve_agent_from_profile appends browser_tool_set after resolve_agent_profile returns, gated on is_tool_usable() and on profile.tools is None — a rule no client can reproduce without duplicating both the default names and the injection logic, and which drifts the moment either changes.
Ask
Have the dry-run also report the resolved outcome — the tool names the agent would actually be constructed with, after create_agent()'s defaulting and the serving layer's injections — alongside the stored tri-state, which is still what gets persisted.
A second, smaller ask that makes this usable in a create flow: let materialize accept a draft profile body, not only a saved profile by name, so an editor can preview before the first save.
Shape is up to you; something like AgentProfileDiagnostics.resolved_tool_names: list[str] next to the existing resolved_mcp_config_keys / resolved_skills, which already follow exactly this "what would actually resolve" pattern.
2. Nothing marks a tool as user-selectable
/server_info's usable_tools and /tools/ both return the process's tool registry. tool_router.py registers four presets unconditionally at import:
register_default_tools(enable_browser=True)
register_builtins_agents(enable_browser=True)
register_gemini_tools(enable_browser=True)
register_planning_tools()
so a stock agent-server advertises eighteen names, of which most should never appear in a profile editor:
| Name | Why not selectable |
|---|---|
edit, read_file, write_file, list_directory |
Gemini preset — a parallel family to file_editor; no product path builds an agent from it |
planning_file_editor |
planning agent's internal PLAN.md editor; needs a plan_path computed per launch, which a stored profile cannot supply |
workflow |
low-level half of a pair whose own docstring says to prefer workflow_tool_set |
task, task_tool_set |
delegation, owned by enable_sub_agents |
FinishTool, ThinkTool, SwitchLLMTool, InvokeSkillTool, VisionInspectTool |
built-ins attached via include_default_tools |
canvas_ui, canvas_ui_control |
client-defined, injected at launch |
The agent settings schema does not help either: its tools field is value_type: array with choices: [].
So a client must maintain a hand-curated list and keep it in sync with SDK releases by hand — and getting it wrong is not cosmetic, since this picker decides what an agent is allowed to do.
Ask
Let a tool declare whether it is user-selectable, and expose that where the tool list is served.
There is a clean precedent: usability is already declared per tool (ToolDefinition.is_usable(), captured into _USABILITY_REG at register_tool time and surfaced through list_usable_tools()). A user_selectable classvar handled the same way would fit the existing shape, default True so nothing changes for third-party tools, and be set False on the built-ins, the delegation pair, the planning editor and the low-level workflow.
Whether the Gemini family is "not selectable" or "selectable but a distinct family" is a product call I would not presume — a family/group label might serve better than a boolean there.
Why both, together
They are the two halves of one question, and answering only the first still leaves clients curating a list by hand. With both, a profile editor becomes: ask the server what can be picked, ask the server what this selection resolves to, render both. No duplicated defaults, no drift on SDK upgrade.
It would also let a client show why a tool is present — default / server-injected / from MCP / attached by a skill — which is the honest answer to "which of these can I actually remove?". Today a user cannot tell an injected tool from a chosen one.
Desired Behavior
The SDK answers both questions, so a client renders what the server reports instead of curating its own lists:
- The dry run behind
POST /api/agent-profiles/{name}/materializereports the tool names the agent would actually be constructed with — aftercreate_agent()'s defaulting and the serving layer's environment-dependent injections — alongside the stored tri-state, and can evaluate a draft profile body so an editor can preview before the first save. - A tool declares whether it is user-selectable, and the served tool list reflects that declaration.
Acceptance Criteria
-
resolve_agent_profile_dry_runreports the tool names the agent would actually be constructed with, including the serving layer's environment-dependent injections. - A profile with
tools: nullreports the standard set;[]reports empty; an explicit list reports itself. The stored tri-state remains separately visible. - Materialize can evaluate a draft profile body, not only a saved profile by name.
- A tool can declare that it is not user-selectable, and the served tool list reflects it.
- The built-ins, the delegation pair, the planning file editor and the low-level
workfloware marked accordingly; a third-party tool that declares nothing stays selectable. - A client that renders the reported lists needs no local copy of
DEFAULT_EXEC_TOOL_NAMESand no hardcoded exclusions.
Relevant code
openhands-sdk/openhands/sdk/profiles/resolver.py—resolve_agent_profile_dry_run,AgentProfileDiagnostics,_build_openhands_settingsopenhands-agent-server/openhands/agent_server/conversation_service.py—_resolve_agent_from_profilebrowser injectionopenhands-sdk/openhands/sdk/tool/registry.py—register_tool,_USABILITY_REG,list_usable_toolsopenhands-agent-server/openhands/agent_server/tool_router.py— the unconditional preset registration- Consumer:
OpenHands/OpenHands#17235(src/constants/profile-tools.tsholds the workaround for both halves) - Related: OpenHands/software-agent-sdk#4953, OpenHands/software-agent-sdk#4956
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.
Assessment
This issue has not been assessed yet.