OpenHands / OpenHands/software-agent-sdk

[Agent Profile] Clients can't ask what tools an agent will get, or which tools are pickable

Open
#4,958 2 comments 0 reactions 1 assignee View on GitHub

@simonrosenberg is already working on this.

Since Sep 17, 2026.

architecture enhancement ready-for-dev
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:

  1. What will this profile's agent actually get?
  2. 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:

  1. The dry run behind POST /api/agent-profiles/{name}/materialize reports the tool names the agent would actually be constructed with — after create_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.
  2. A tool declares whether it is user-selectable, and the served tool list reflects that declaration.
Acceptance Criteria
  • resolve_agent_profile_dry_run reports the tool names the agent would actually be constructed with, including the serving layer's environment-dependent injections.
  • A profile with tools: null reports 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 workflow are 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_NAMES and no hardcoded exclusions.

Relevant code

  • openhands-sdk/openhands/sdk/profiles/resolver.pyresolve_agent_profile_dry_run, AgentProfileDiagnostics, _build_openhands_settings
  • openhands-agent-server/openhands/agent_server/conversation_service.py_resolve_agent_from_profile browser injection
  • openhands-sdk/openhands/sdk/tool/registry.pyregister_tool, _USABILITY_REG, list_usable_tools
  • openhands-agent-server/openhands/agent_server/tool_router.py — the unconditional preset registration
  • Consumer: OpenHands/OpenHands#17235 (src/constants/profile-tools.ts holds the workaround for both halves)
  • Related: OpenHands/software-agent-sdk#4953, OpenHands/software-agent-sdk#4956

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.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.