Skip to content

Commit 7219299

Browse files
authored
feat: add stop_reason field to ResultMessage (#619)
Adds `stop_reason: str | None` to `ResultMessage`, matching the TypeScript SDK. The field carries the API's stop reason (`"end_turn"`, `"max_tokens"`, etc.) through to the result message. Backward-compatible: defaults to `None` and the parser uses `data.get("stop_reason")`, so older CLI output without the field produces `None` rather than failing. <!-- CHANGELOG:START --> - Add `stop_reason: str | None` field to `ResultMessage` <!-- CHANGELOG:END --> ## Test plan - Unit: parser tests for explicit value, explicit `null` → `None`, and absent field → `None` - E2E: verified against CLI v2.1.64-dev — SDK parsing correct (CLI-side population tracked separately)
1 parent d9fe709 commit 7219299

3 files changed

Lines changed: 41 additions & 0 deletions

File tree

src/claude_agent_sdk/_internal/message_parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ def parse_message(data: dict[str, Any]) -> Message | None:
153153
is_error=data["is_error"],
154154
num_turns=data["num_turns"],
155155
session_id=data["session_id"],
156+
stop_reason=data.get("stop_reason"),
156157
total_cost_usd=data.get("total_cost_usd"),
157158
usage=data.get("usage"),
158159
result=data.get("result"),

src/claude_agent_sdk/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -677,6 +677,7 @@ class ResultMessage:
677677
is_error: bool
678678
num_turns: int
679679
session_id: str
680+
stop_reason: str | None = None
680681
total_cost_usd: float | None = None
681682
usage: dict[str, Any] | None = None
682683
result: str | None = None

tests/test_message_parser.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,45 @@ def test_parse_valid_result_message(self):
313313
message = parse_message(data)
314314
assert isinstance(message, ResultMessage)
315315
assert message.subtype == "success"
316+
assert message.stop_reason is None
317+
318+
def test_parse_result_message_with_stop_reason(self):
319+
"""Test parsing a result message with stop_reason field.
320+
321+
The stop_reason field mirrors the Anthropic API's stop_reason on the
322+
final assistant turn (e.g., "end_turn", "max_tokens", "tool_use").
323+
"""
324+
data = {
325+
"type": "result",
326+
"subtype": "success",
327+
"duration_ms": 1000,
328+
"duration_api_ms": 500,
329+
"is_error": False,
330+
"num_turns": 2,
331+
"session_id": "session_123",
332+
"stop_reason": "end_turn",
333+
"result": "Done",
334+
}
335+
message = parse_message(data)
336+
assert isinstance(message, ResultMessage)
337+
assert message.stop_reason == "end_turn"
338+
assert message.result == "Done"
339+
340+
def test_parse_result_message_with_null_stop_reason(self):
341+
"""Test parsing a result message with explicit null stop_reason."""
342+
data = {
343+
"type": "result",
344+
"subtype": "error_max_turns",
345+
"duration_ms": 1000,
346+
"duration_api_ms": 500,
347+
"is_error": True,
348+
"num_turns": 10,
349+
"session_id": "session_123",
350+
"stop_reason": None,
351+
}
352+
message = parse_message(data)
353+
assert isinstance(message, ResultMessage)
354+
assert message.stop_reason is None
316355

317356
def test_parse_invalid_data_type(self):
318357
"""Test that non-dict data raises MessageParseError."""

0 commit comments

Comments
 (0)