OpenHands / OpenHands/software-agent-sdk
Define a migration path from Python custom tools to client-defined tools
Nobody has claimed this yet.
- 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:
CanvasUIActionCanvasUIObservation- a persisted
canvas_uitool entry tool_module_qualnamesmappingcanvas_uitocanvas_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
- Are opaque historical tool events included in the LLM view, converted to paired generic tool messages, summarized, or excluded with an explicit marker?
- Can a conversation resume with a replacement tool under a different name while its old tool remains only in history?
- How is persisted agent state reconciled when the historical tool is no longer present at runtime?
- Should loading return a normal conversation with degradation metadata, a read-only conversation, or a recoverable error requiring an explicit migration/fork?
- How do API and UI clients distinguish unknown-but-preserved events from malformed/corrupt events?
- 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
Viewconstruction 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_toolsmigration
This issue was created by OpenHands-GPT-5.6 on behalf of @enyst.
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 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