langchain-ai/deepagents · error · ValueError

ReadResult.total_lines requires start_line and end_line to b

Error message

ReadResult.total_lines requires start_line and end_line to be set

What it means

ReadResult.__post_init__ validates that any backend populating `total_lines` on a paginated read also populates the window fields `start_line` and `end_line`. `total_lines` is only meaningful relative to a known window, so a dataclass built with total_lines but no line window violates the protocol invariant and is rejected at construction time with a ValueError. This is a contract check that keeps ReadResult self-consistent for downstream line-number formatting and pagination logic.

Source

Thrown at libs/deepagents/deepagents/backends/protocol.py:261

        immediately after the last one shown (`next_offset == end_line`, since
        `end_line` is 1-indexed). Fail loudly here to keep a backend from
        emitting a `next_offset` that would silently skip unshown source lines
        once it reaches the middleware.
        """
        if (self.start_line is None) != (self.end_line is None):
            msg = "ReadResult.start_line and end_line must be set together or both left unset"
            raise ValueError(msg)
        if self.no_lines_requested and (
            self.error is not None or self.start_line is not None or self.next_offset is not None or self.total_lines is not None
        ):
            msg = "ReadResult.no_lines_requested describes an uninspected window; it cannot be combined with error or pagination fields"
            raise ValueError(msg)
        if self.next_offset is not None and self.start_line is None:
            msg = "ReadResult.next_offset requires start_line and end_line to be set"
            raise ValueError(msg)
        if self.total_lines is not None and self.start_line is None:
            msg = "ReadResult.total_lines requires start_line and end_line to be set"
            raise ValueError(msg)

        # Numeric consistency of a present window. `start_line`/`end_line` are
        # bound together above, so testing `start_line` covers both.
        if self.start_line is not None and self.end_line is not None:
            if self.start_line < 1 or self.end_line < self.start_line:
                msg = f"ReadResult window must satisfy 1 <= start_line <= end_line, got start_line={self.start_line}, end_line={self.end_line}"
                raise ValueError(msg)
            if self.total_lines is not None and self.total_lines < self.end_line:
                msg = f"ReadResult.total_lines ({self.total_lines}) cannot be less than end_line ({self.end_line})"
                raise ValueError(msg)
            if self.next_offset is not None and self.next_offset != self.end_line:
                msg = f"ReadResult.next_offset ({self.next_offset}) must equal end_line ({self.end_line}), the 0-indexed line after the last shown"
                raise ValueError(msg)


@dataclass
class WriteResult:
    """Result from backend `write` operations.

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Set start_line and end_line on the ReadResult whenever you set total_lines (1-based, inclusive window).
  2. If the read was unpaginated, compute the window from the content (start_line=1, end_line=number of lines returned) instead of dropping the fields.
  3. If you do not have window metadata, omit total_lines entirely rather than returning it alone.

Example fix

// before
return ReadResult(content=text, total_lines=len(all_lines))
// after
return ReadResult(
    content=text,
    start_line=1,
    end_line=len(lines_returned),
    total_lines=len(all_lines),
)
Defensive patterns

Strategy: validation

Validate before calling

def is_valid_read_result_fields(**fields):
    if fields.get('total_lines') is not None:
        return fields.get('start_line') is not None and fields.get('end_line') is not None
    return True
# call before constructing: is_valid_read_result_fields(total_lines=120, start_line=None, end_line=None) -> False

Type guard

def has_consistent_window(r) -> bool:
    return not (r.total_lines is not None and (r.start_line is None or r.end_line is None))

Try / catch

try:
    result = ReadResult(content=text, total_lines=n)
except ValueError as e:
    logger.warning('invalid ReadResult: %s', e)
    result = ReadResult(content=text, start_line=1, end_line=len(text.splitlines()), total_lines=n)

Prevention

When it happens

Trigger: Constructing ReadResult(content=..., total_lines=120) without setting start_line/end_line. Typically a custom backend's `read` implementation returns total_lines to report file size but omits the window fields, or builds the result via a helper that only forwards some fields.

Common situations: Writing a custom Backend subclass implementing `read`; refactoring a backend after the protocol added the line-window fields; hand-building ReadResult in tests with partial fields.

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 langchain-ai/deepagents@a1af029e6e (2026-08-29). Data as JSON: /api/errors/7db233fb1219912f. Report an issue: GitHub.