OpenHands / OpenHands/software-agent-sdk

Define a migration path from Python custom tools to client-defined tools

Open
#4,118 2 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement proposal
Dominant language
Python
Stars
1.1k
Forks
542
Avg merge
1d 19h
Merged PRs (30d)
137

Description

Summary

Define a supported migration path for products replacing Python-defined custom tools with JSON client_tools, without requiring the legacy Python module forever just to read persisted conversations.

The preferred baseline should be tolerant loading of unknown historical action/observation kinds: preserve their raw payload and identity in an opaque placeholder rather than failing conversation restoration. This would make tool implementations evolvable while keeping historical data accessible.

This is not a duplicate of #4080. That issue reports the concrete failure mode where one unknown event kind drops an entire conversation. This issue asks for the broader tool-migration contract: after tolerant loading succeeds, how should persisted agent tool state, historical event rendering, LLM context, and future tool execution behave when an old Python tool has been replaced by a client-defined tool?

Concrete case: Agent Canvas

OpenHands/agent-canvas#1797 migrates canvas_ui from a Python ToolDefinition to the SDK JSON client_tools API.

New conversations no longer need Python code to define or execute the tool, but old persisted conversations can contain:

  • CanvasUIAction
  • CanvasUIObservation
  • a persisted canvas_ui tool entry
  • tool_module_qualnames mapping canvas_ui to canvas_ui_tool

Today, restoring those conversations still requires shipping and importing the old Python module so the SDK can register the historical classes before deserialization. A TypeScript client cannot shim this boundary because agent-server deserializes the persisted event log before sending events to the client.

There is also a naming constraint: the replacement client tool cannot safely reuse canvas_ui while the legacy registration may coexist in the same process, because generated client action kinds and tool registrations are process-global. Canvas currently uses canvas_ui_client and maps both event generations to the same visible behavior.

This means the original goal of #3305 — eliminating Python tool code and import plumbing from frontend repositories — is complete for new conversations but not for existing installations.

Migration options to define

1. Opaque fallback for unknown historical events — preferred baseline

When an ActionEvent or ObservationEvent contains an unregistered nested kind, deserialize it into an explicit opaque/unknown representation that preserves at least:

  • the complete raw payload;
  • event ID and parent ID;
  • source and timestamp;
  • tool name, tool call ID, action ID, and LLM response ID;
  • action/observation pairing information.

Unknown historical events must never execute code. They should remain inspectable and available to API/UI clients, with a clear degraded/unknown marker.

This is the dummy action/observation direction proposed in #2667 and the per-event graceful-degradation direction in #4080.

2. Historical kind aliases or tombstones

Allow a replacement tool/plugin to declare that old kinds are readable aliases or tombstones, without restoring the old executor. This may provide richer rendering than an opaque fallback, but it still requires migration metadata and should not be necessary merely to make a conversation load.

3. Versioned event migrations

Provide SDK-owned migrations from historical payloads/kinds to a current representation, similar in spirit to the persisted settings migrations discussed in #2788. This gives the cleanest canonical state but requires version metadata, idempotency, rollback/error handling, and clear ownership of migrations from third-party tools.

4. Fork/condense onto a new toolset

After historical events can be read tolerantly, optionally condense or fork the conversation onto the replacement toolset, as discussed in #2667 and #1787. This may be useful for continuing the agent safely, but it cannot be the first recovery step: the SDK must be able to read the old events before it can condense them.

Questions the contract should answer

  1. Are opaque historical tool events included in the LLM view, converted to paired generic tool messages, summarized, or excluded with an explicit marker?
  2. Can a conversation resume with a replacement tool under a different name while its old tool remains only in history?
  3. How is persisted agent state reconciled when the historical tool is no longer present at runtime?
  4. Should loading return a normal conversation with degradation metadata, a read-only conversation, or a recoverable error requiring an explicit migration/fork?
  5. How do API and UI clients distinguish unknown-but-preserved events from malformed/corrupt events?
  6. What guarantees prevent an unknown historical payload from triggering an executor or dynamic import?

Proposed acceptance criteria

  • A persisted conversation containing an unregistered historical action and observation can be listed and loaded after a process restart without importing their original Python module.
  • The original raw payload and event/tool correlation IDs survive loading and API serialization.
  • Unknown historical events never execute and are clearly marked as degraded/opaque.
  • One unknown historical event does not hide or invalidate otherwise readable conversation history.
  • The SDK defines how unknown action/observation pairs affect View construction and LLM message conversion.
  • A conversation can adopt a replacement toolset—or be explicitly forked/condensed onto it—without retaining the old executor.
  • An integration test covers migration from a Python custom tool to a differently named JSON client tool, then restores the old conversation with the Python module absent.
  • Documentation gives tool authors a recommended deprecation/migration lifecycle.

Related

  • #4080 — one unregistered event kind fails the entire conversation load
  • #2667 — research strategies for toolset changes and persisted sessions
  • #3305 — client-defined tools via JSON
  • #1787 — fork a conversation when tools change
  • #2788 — versioned migrations for persisted AgentSettings
  • OpenHands/agent-canvas#1797 — concrete Python custom-tool → client_tools migration

This issue was created by OpenHands-GPT-5.6 on behalf of @enyst.

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 by reading the related issues #4080, #2667, and #3305, then trace ActionEvent and ObservationEvent deserialization, persisted tool state, View construction, and LLM message conversion. Define the migration contract for opaque historical events, replacement tools, and safe loading without the original Python module. Done includes the listed integration test and documentation for the tool deprecation lifecycle.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
api, 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.