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
- Set the message's role to 'toolResult' in the payload
- Fix the server-side emitter to send tool results with role 'toolResult'
- Use parse_agent_messages for payloads of mixed/unknown roles instead of the tool-result-specific parser
- 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
- Use the exact literal 'toolResult' (camelCase) in payloads
- Route mixed-role payloads through parse_agent_messages
- Pin matching protocol versions on both sides
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
- {field}.role must be 'assistant'
- runInTerminal request did not include a command
- ${label} must be a string
- Unknown daemon state: ${state}
- daemon.readyPending must be an array
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/75250328c0dad42b.
Report an issue: GitHub.