can1357/oh-my-pi · error · RpcError
Host tool handlers must return a string or a result mapping
Error message
Host tool handlers must return a string or a result mapping
What it means
RpcError raised by _normalize_host_tool_result when a host tool handler returns something other than a str or a Mapping. The client must serialize handler output into a JsonObject tool-result ({content: [...]}) and refuses unknown return shapes.
Source
Thrown at python/omp-rpc/src/omp_rpc/client.py:1428
error=str(response.get("error", "")),
code=raw_code if isinstance(raw_code, str) else None,
)
data = response.get("data")
if data is None:
return {}
return _clone_json_object(data)
def _send_notification(self, payload: JsonObject) -> None:
process = self._require_process()
self._write_json(process, payload)
def _normalize_host_tool_result(self, result: object) -> JsonObject:
if isinstance(result, str):
return {"content": [{"type": "text", "text": result}]}
if isinstance(result, Mapping):
return cast(JsonObject, dict(result))
raise RpcError("Host tool handlers must return a string or a result mapping")
def _normalize_host_tool_event(self, payload: JsonObject) -> None:
"""Rename transport tool events for in-flight host-tool dispatches.
With `tools.xdev` enabled, omp mounts custom tools as `xd://` devices
and the agent invokes them through the `write` tool, so
`tool_execution_update`/`tool_execution_end` events report the
transport tool (`write`) rather than the host tool that actually ran.
The `host_tool_call` frame carries the outer call's `toolCallId` (the
device dispatch forwards it verbatim), which lets events for that call
be renamed to the executed host tool — consumers observe the same tool
names regardless of transport. A top-level call (xdev off) maps the
name onto itself. `tool_execution_start` precedes the `host_tool_call`
frame on the wire, so start events keep the transport name.
"""
tool_call_id = payload.get("toolCallId")
if not isinstance(tool_call_id, str):
returnView on GitHub (pinned to 9690622007)
Solutions
- Return a plain string (treated as a text content block) from the handler
- Return a dict/Mapping matching the tool-result shape, e.g. {"content": [{"type": "text", "text": ...}], ...}
- Wrap arbitrary objects: str(result) or asdict(result) before returning
- Ensure every code path in the handler returns, including early exits
Example fix
// before
def handler(ctx):
compute_something() # returns None
// after
def handler(ctx):
result = compute_something()
return {"content": [{"type": "text", "text": str(result)}]} Defensive patterns
Strategy: type-guard
Validate before calling
def validate_tool_result(result: object) -> bool:
return isinstance(result, (str, Mapping)) Type guard
def is_valid_host_tool_result(result: object) -> TypeGuard[str | Mapping]:
return isinstance(result, (str, Mapping)) Try / catch
def safe_handler(ctx):
result = do_work(ctx)
if not isinstance(result, (str, Mapping)):
result = str(result) # coerce unexpected shapes
return result Prevention
- Type-annotate host tool handlers as Callable[..., str | Mapping]
- Ensure every handler path returns a value
- Run type checking (mypy/pyright) over host tool registrations
When it happens
Trigger: Registering a host tool whose callable returns None, an int/list/dataclass, or forgetting a return statement; a handler that returns a Pydantic/ORM object instead of a dict.
Common situations: Writing custom host tools and returning the tool's internal result object directly; returning None on an early-exit path; returning a list of content blocks instead of the full result mapping.
Related errors
- Invalid pattern: {err}
- RPC payload objects must use string keys
- set_host_tools response did not include toolNames
- Unsupported language '{value}'. Supported: {}
- Unable to infer language from file extension: {}. Specify `l
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/34d244404e45f34f.
Report an issue: GitHub.