langchain-ai/deepagents · error · ValueError

row has {len} cells, expected {expected}

Error message

row has {len} cells, expected {expected}

What it means

`_markdown_table` renders rows into a Markdown table and refuses ragged rows: if any row's cell count differs from the header count, it raises this ValueError, because Markdown silently drops surplus cells and pads missing ones — corrupting the table with no diagnostic.

Source

Thrown at libs/code/deepagents_code/app.py:2459

    Every header and cell is escaped via `_escape_markdown`, so external text
    can neither forge additional cells with `|` nor be parsed as markdown.

    Args:
        headers: Column headings.
        rows: One sequence of cells per row, each matching `headers` in length.

    Returns:
        The table as markdown source.

    Raises:
        ValueError: If a row's length does not match `headers`. Checked because
            markdown silently drops surplus cells and pads missing ones, so a
            ragged row would corrupt the table with no other diagnostic.
    """
    for row in rows:
        if len(row) != len(headers):
            msg = f"row has {len(row)} cells, expected {len(headers)}"
            raise ValueError(msg)
    lines = [
        "| " + " | ".join(_escape_markdown(header) for header in headers) + " |",
        "| " + " | ".join("---" for _ in headers) + " |",
    ]
    lines.extend(
        "| " + " | ".join(_escape_markdown(cell) for cell in row) + " |" for row in rows
    )
    return "\n".join(lines)


def _log_task_exception(task: asyncio.Task[Any]) -> None:
    """Done-callback that surfaces unhandled exceptions from fire-and-forget tasks.

    Default `asyncio` behavior is to log "Task exception was never retrieved"
    only when the task is GC'd — easy to miss. This callback runs at task
    completion and routes failures through `logger.warning` with `exc_info`,
    matching the codebase pattern at `_finalize_git_branch_refresh`. Use
    when scheduling a coroutine via `asyncio.create_task` whose result is

View on GitHub (pinned to a1af029e6e)

Solutions

  1. Pad short rows to the header length and truncate long rows before calling the renderer
  2. Fix the data source so every row has exactly len(headers) cells
  3. Assert row lengths in a unit test when constructing rows dynamically

Example fix

// before
rows = [("a", "b"), ("c",)]
_markdown_table(headers, rows)
// after
rows = [tuple(row) + ("",) * (len(headers) - len(row)) if len(row) < len(headers) else row[:len(headers)] for row in rows]
_markdown_table(headers, rows)
Defensive patterns

Strategy: validation

Validate before calling

if any(len(row) != len(headers) for row in rows):
    raise ValueError("ragged table rows before rendering")

Type guard

def is_rectangular(headers: list[str], rows: list[list[str]]) -> bool:
    return all(len(row) == len(headers) for row in rows)

Try / catch

try:
    table = _markdown_table(headers, rows)
except ValueError as exc:
    logger.error("table render failed: %s", exc)
    table = ""  # or fall back to a list rendering

Prevention

When it happens

Trigger: Calling `_markdown_table` (directly or via `_render_extensions` / `_render_tool_catalog`) with a row list whose lengths vary or don't match `len(headers)` — e.g. a source returning a variable-arity tuple per row.

Common situations: Extension metadata or tool catalog data where one entry gained/lost a field; joining two data sources with different column counts; optional trailing fields omitted in one row.


AI-assisted analysis of langchain-ai/deepagents@a1af029e6e (2026-08-29). Data as JSON: /api/errors/e1d63a41e8f2768f. Report an issue: GitHub.