{"record":{"id":"34d244404e45f34f","repo":"can1357/oh-my-pi","slug":"host-tool-handlers-must-return-a-string-or-a-resul","errorCode":null,"errorMessage":"Host tool handlers must return a string or a result mapping","messagePattern":"Host tool handlers must return a string or a result mapping","errorType":"exception","errorClass":"RpcError","httpStatus":null,"severity":"error","filePath":"python/omp-rpc/src/omp_rpc/client.py","lineNumber":1428,"sourceCode":"                error=str(response.get(\"error\", \"\")),\n                code=raw_code if isinstance(raw_code, str) else None,\n            )\n\n        data = response.get(\"data\")\n        if data is None:\n            return {}\n        return _clone_json_object(data)\n\n    def _send_notification(self, payload: JsonObject) -> None:\n        process = self._require_process()\n        self._write_json(process, payload)\n\n    def _normalize_host_tool_result(self, result: object) -> JsonObject:\n        if isinstance(result, str):\n            return {\"content\": [{\"type\": \"text\", \"text\": result}]}\n        if isinstance(result, Mapping):\n            return cast(JsonObject, dict(result))\n        raise RpcError(\"Host tool handlers must return a string or a result mapping\")\n\n    def _normalize_host_tool_event(self, payload: JsonObject) -> None:\n        \"\"\"Rename transport tool events for in-flight host-tool dispatches.\n\n        With `tools.xdev` enabled, omp mounts custom tools as `xd://` devices\n        and the agent invokes them through the `write` tool, so\n        `tool_execution_update`/`tool_execution_end` events report the\n        transport tool (`write`) rather than the host tool that actually ran.\n        The `host_tool_call` frame carries the outer call's `toolCallId` (the\n        device dispatch forwards it verbatim), which lets events for that call\n        be renamed to the executed host tool — consumers observe the same tool\n        names regardless of transport. A top-level call (xdev off) maps the\n        name onto itself. `tool_execution_start` precedes the `host_tool_call`\n        frame on the wire, so start events keep the transport name.\n        \"\"\"\n        tool_call_id = payload.get(\"toolCallId\")\n        if not isinstance(tool_call_id, str):\n            return","sourceCodeStart":1410,"sourceCodeEnd":1446,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/python/omp-rpc/src/omp_rpc/client.py#L1410-L1446","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\ndef handler(ctx):\n    compute_something()  # returns None\n// after\ndef handler(ctx):\n    result = compute_something()\n    return {\"content\": [{\"type\": \"text\", \"text\": str(result)}]}","handlingStrategy":"type-guard","validationCode":"def validate_tool_result(result: object) -> bool:\n    return isinstance(result, (str, Mapping))","typeGuard":"def is_valid_host_tool_result(result: object) -> TypeGuard[str | Mapping]:\n    return isinstance(result, (str, Mapping))","tryCatchPattern":"def safe_handler(ctx):\n    result = do_work(ctx)\n    if not isinstance(result, (str, Mapping)):\n        result = str(result)  # coerce unexpected shapes\n    return result","preventionTips":["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"],"tags":["rpc","host-tools","contract-violation","type-error"],"backgroundTag":"invalid-return-type","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}