anthropics / anthropics/claude-agent-sdk-python

parse_message silently drops newer CLI wire fields (ttft_ms, terminal_reason, stop_details, …) with no escape hatch

Open
#1,026 0 comments 0 reactions 0 assignees View on GitHub
bug enhancement
Dominant language
Python
Stars
8.1k
Forks
1.3k
Avg merge
2d 31m
Merged PRs (30d)
1

Description

## Summary

The Claude Code CLI (2.1.150) emits several stream-json fields that `parse_message` neither reads nor retains — they vanish from the typed messages with no way to recover them. Unlike `SystemMessage` (which keeps the raw frame as `.data`), `AssistantMessage` and `ResultMessage` don't retain the original frame, so SDK consumers cannot access these fields at all without bypassing the SDK and re-parsing the wire themselves.

Verified identical on **claude-agent-sdk 0.2.87 and 0.2.95** (latest).

## Dropped fields observed on the wire

| Wire field | Example recorded value | Notes |
|---|---|---|
| `result.ttft_ms` | `2806` | time-to-first-token — a metric consumers chart |
| `result.terminal_reason` | `"completed"` | |
| `result.fast_mode_state` | `"off"` | |
| `assistant message.content[].caller` | `{"type": "direct"}` | per content block |
| `assistant message.stop_details` | observed `null` | emitted, unmodeled |
| `assistant message.diagnostics` | observed `null` | emitted, unmodeled |
| `assistant message.context_management` | observed `null` | emitted, unmodeled |
| `assistant request_id` | `"req_…"` | top-level frame key |

## Repro

```python
from claude_agent_sdk._internal.message_parser import parse_message

frame = { # verbatim CLI stream-json (trimmed)
"type": "result", "subtype": "success", "duration_ms": 8127,
"duration_api_ms": 7903, "is_error": False, "num_turns": 4,
"session_id": "s", "total_cost_usd": 0.02, "usage": {},
"ttft_ms": 2806, "terminal_reason": "completed", "fast_mode_state": "off",
}
msg = parse_message(frame)
assert not hasattr(msg, "ttft_ms") # gone
assert "terminal_reason" not in str(msg) # gone, and not recoverable
```

## Suggested fix (either works)

1. Model the fields on the dataclasses (`ResultMessage.ttft_ms`, etc.), or
2. Retain the raw frame on every message type (the `SystemMessage.data` pattern) as a forward-compatible escape hatch, so new CLI fields are reachable before they're modeled.

## How this was found

Mechanically, by a record/replay drift detector ([claude-agent-cassette](https://github.com/oneryalcin/claude-agent-cassette)): it runs `parse_message` over an access-tracking view of recorded frames and reports any key the parser neither reads nor retains. Happy to re-run it against a branch if useful.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.