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):
            return

View on GitHub (pinned to 9690622007)

Solutions

  1. Return a plain string (treated as a text content block) from the handler
  2. Return a dict/Mapping matching the tool-result shape, e.g. {"content": [{"type": "text", "text": ...}], ...}
  3. Wrap arbitrary objects: str(result) or asdict(result) before returning
  4. 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

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


AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31). Data as JSON: /api/errors/34d244404e45f34f. Report an issue: GitHub.