#863

[BUG] claude_agent_sdk/05_Building_a_session_browser.ipynb, cell 14: message renderer prints the user's prompt as blank whenever `content` is a string

OpenHetCreepopened 1mo ago

Affected Notebook/File

claude_agent_sdk/05_Building_a_session_browser.ipynb

Bug Description

Cell 14 renders each message like this:

for m in messages:
    role = m.type
    # content is a list of blocks; pull out the text ones
    text_parts = [
        b.get("text", "")
        for b in m.message.get("content", [])
        if isinstance(b, dict) and b.get("type") == "text"
    ]
    text = " ".join(text_parts).strip()
    print(f"[{role:>9}] {text[:100]}")

m.message is whatever get_session_messages() read back from the session's JSONL transcript, completely unchanged: SessionMessage.message is typed Any and documented "Raw Anthropic API message dict (role, content, etc.)" (claude_agent_sdk/types.py), and the function that builds it does no normalization at all (claude_agent_sdk/_internal/sessions.py: message=entry.get("message")).

The Anthropic Messages API's content field is documented as string-or-block-array, and a plain, interactively-typed prompt — i.e. the normal case — is stored as a plain string. When content is a string, the list comprehension iterates it character by character; isinstance(b, dict) is False for every character, so text_parts is always []. Nothing raises — the cell just prints an empty line where the prompt should be.

The notebook's own markdown even names the shape this breaks on: cell 13 describes message as "a message dict in the same shape as the Anthropic Messages API (role, content)" — and the Messages API's own content field is string-or-array — while cell 14's inline comment asserts the opposite ("content is a list of blocks").

I checked every Claude Code session transcript on my machine — 388 files — and every single one renders the opening user turn as an empty string: 384 because that message's content is a plain string, and the remaining 4 because it happens to be an array whose blocks are all tool_result with no text block either. Neither shape survives the filter.

Expected: the user's actual prompt is printed. Actual: an empty string, silently, on effectively every real session.

Suggested fix — branch on the type before filtering:

content = m.message.get("content", [])
if isinstance(content, str):
    text = content.strip()
else:
    text = " ".join(
        b.get("text", "") for b in content if isinstance(b, dict) and b.get("type") == "text"
    ).strip()

Steps to Reproduce

No SDK install and no API key needed — get_session_messages() just replays the raw JSONL, so applying cell 14's own filter directly to any transcript your own Claude Code CLI has already written reproduces it exactly:

import glob, json, os

paths = glob.glob(os.path.join(os.path.expanduser("~"), ".claude", "projects", "*", "*.jsonl"))
with open(paths[0], encoding="utf-8") as f:
    for line in f:
        line = line.strip()
        if not line:
            continue
        entry = json.loads(line)
        if entry.get("type") != "user" or entry.get("isMeta") or entry.get("isSidechain"):
            continue
        content = entry["message"].get("content")
        print("typeof content:", type(content).__name__)
        text_parts = [b.get("text", "") for b in (content or []) if isinstance(b, dict) and b.get("type") == "text"]
        print("cell 14 would print:", repr(" ".join(text_parts).strip()))
        break

On my machine this printed typeof content: str / cell 14 would print: '' for all 388 transcripts checked (script above tested and run, not just described).

Or with the notebook itself: run cells 1-12 to create the demo sessions, then run cell 14 and compare its printed [ user] line against the prompt you actually typed into query() two cells earlier.

Error Message

N/A — nothing raises. That's what makes this one easy to miss: the notebook ships with every
cell's output cleared, so a reader only sees the blank line once they run it themselves, and it
reads as a formatting quirk rather than a bug.

Environment

  • Python version: 3.11+ (this recipe's own prerequisite); the reproduction above needs nothing beyond the standard library
  • OS: not version-sensitive — reproduces on any machine that has used the Claude Code CLI
  • claude-agent-sdk version: reproduces at this notebook's own pinned floor (>=0.1.51) through the current release (0.2.152) — the file and line that hand back the raw value are unchanged across that whole range

Would you be willing to submit a PR to fix this?

No, I'm just reporting

Activity

0

No activity yet.