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()))
breakOn 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
0No activity yet.