can1357/oh-my-pi · error · ValueError

{field}.role must be 'toolResult'

Error message

{field}.role must be 'toolResult'

What it means

The parser distinguishes tool-result messages from other agent messages by their role literal. _parse_tool_result_message raises this error when the payload's role is a valid agent-message role but not 'toolResult'. The check exists so the subsequent cast to ToolResultMessage is sound and callers can rely on the shape.

Source

Thrown at python/omp-rpc/src/omp_rpc/protocol.py:307

def _parse_agent_message(payload: JsonObject, *, field: str) -> AgentMessage:
    _require_literal(
        payload.get("role"), _AGENT_MESSAGE_ROLE_VALUES, field=f"{field}.role"
    )
    return cast(AgentMessage, _clone_json_object(payload, field=field))


def _parse_assistant_message(payload: JsonObject, *, field: str) -> AssistantMessage:
    message = _parse_agent_message(payload, field=field)
    if message.get("role") != "assistant":
        raise ValueError(f"{field}.role must be 'assistant'")
    return cast(AssistantMessage, message)


def _parse_tool_result_message(payload: JsonObject, *, field: str) -> ToolResultMessage:
    message = _parse_agent_message(payload, field=field)
    if message.get("role") != "toolResult":
        raise ValueError(f"{field}.role must be 'toolResult'")
    return cast(ToolResultMessage, message)


def parse_agent_messages(payload: JsonValue | None) -> tuple[AgentMessage, ...]:
    if payload is None:
        return ()
    if not isinstance(payload, list):
        raise ValueError("messages must be a list")

    messages: list[AgentMessage] = []
    for index, item in enumerate(payload):
        messages.append(
            _parse_agent_message(
                _clone_json_object(item, field=f"messages[{index}]"),
                field=f"messages[{index}]",
            )
        )
    return tuple(messages)

View on GitHub (pinned to 9690622007)

Solutions

  1. Set the message's role to 'toolResult' in the payload
  2. Fix the server-side emitter to send tool results with role 'toolResult'
  3. Use parse_agent_messages for payloads of mixed/unknown roles instead of the tool-result-specific parser
  4. Verify client/server protocol versions agree on the role literal

Example fix

// before
{"role": "tool_result", "toolCallId": "t1", ...}
// after
{"role": "toolResult", "toolCallId": "t1", ...}
Defensive patterns

Strategy: type-guard

Validate before calling

def is_tool_result(m: dict) -> bool:
    return isinstance(m, dict) and m.get("role") == "toolResult"

if not is_tool_result(msg):
    raise ValueError("expected a toolResult message")

Type guard

from typing import TypeGuard

def is_tool_result(m: object) -> TypeGuard[dict]:
    return isinstance(m, dict) and m.get("role") == "toolResult"

Try / catch

try:
    notification = parse_notification(payload)
except ValueError as e:
    logger.warning("malformed toolResult notification: %s", e)
    notification = None

Prevention

When it happens

Trigger: Calling parse_notification with a notification whose field carries a message with role 'assistant' or 'user' where a toolResult is required; a server emitting tool results under the wrong role; hand-built payloads with a typo like 'tool_result'.

Common situations: Custom server integrations mislabeling tool results; protocol migrations that changed role casing; test fixtures copied from assistant messages.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


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