Source code for ralph.agents.parsers.base
"""Base types for agent output parsing.
This module defines the parser protocol and shared text-block helpers.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Protocol, runtime_checkable
if TYPE_CHECKING:
from collections.abc import Iterator
from .agent_output_line import AgentOutputLine
def _multimodal_block_summary(block: dict[str, object]) -> str | None:
"""Return a bounded readable summary for a multimodal content block, or None.
Returns a short human-readable placeholder for image and resource_reference
blocks so they are never silently dropped when only text can be emitted.
"""
block_type = str(block.get("type", ""))
if block_type == "image":
source = block.get("source") or block.get("data") or {}
mime = (
(source.get("media_type") if isinstance(source, dict) else None)
or block.get("mimeType")
or block.get("mime_type")
or "image"
)
return f"[image: {mime}]"
if block_type == "resource_reference":
uri = block.get("uri", "")
modality = block.get("modality", "media")
return f"[{modality}: {uri}]"
return None
[docs]
def stringify_text_blocks(value: object, *, require_text_type: bool = False) -> str:
"""Extract text from a string or a list of text-block dicts.
Args:
value: A plain string, or a list of dicts with a 'text' field.
require_text_type: When True, only include dicts where type=='text' (Claude
tool_result rule). When False, include any dict with a 'text' key
(OpenCode output rule). In both modes, multimodal blocks (image,
resource_reference) emit a bounded readable placeholder rather than
being silently dropped.
"""
if isinstance(value, str):
return value
if isinstance(value, list):
parts: list[str] = []
for item in value:
if not isinstance(item, dict):
continue
item_type = str(item.get("type", ""))
if require_text_type:
if item_type == "text":
text = str(item.get("text", ""))
if text:
parts.append(text)
continue
elif "text" in item:
text = str(item.get("text", ""))
if text:
parts.append(text)
continue
summary = _multimodal_block_summary(item)
if summary is not None:
parts.append(summary)
if parts:
return "\n".join(part for part in parts if part)
return str(value)
[docs]
@runtime_checkable
class AgentParser(Protocol):
"""Protocol all parser modules must implement.
A parser takes raw lines from an agent's stdout and yields
normalized AgentOutputLine instances.
"""
[docs]
def parse(self, lines: Iterator[str]) -> Iterator[AgentOutputLine]:
"""Parse agent output lines.
Args:
lines: Iterator of raw lines from agent stdout.
Yields:
Normalized AgentOutputLine instances.
"""
...