Skip to content

Commit 876d4db

Browse files
authored
Merge branch 'anthropics:main' into main
2 parents 77da402 + 146e3d6 commit 876d4db

File tree

9 files changed

+117
-13
lines changed

9 files changed

+117
-13
lines changed

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
# Changelog
22

3+
## 0.1.39
4+
5+
### Internal/Other Changes
6+
7+
- Updated bundled Claude CLI to version 2.1.49
8+
9+
## 0.1.38
10+
11+
### Internal/Other Changes
12+
13+
- Updated bundled Claude CLI to version 2.1.47
14+
15+
## 0.1.37
16+
17+
### Internal/Other Changes
18+
19+
- Updated bundled Claude CLI to version 2.1.44
20+
321
## 0.1.36
422

523
### New Features

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "claude-agent-sdk"
7-
version = "0.1.36"
7+
version = "0.1.39"
88
description = "Python SDK for Claude Code"
99
readme = "README.md"
1010
requires-python = ">=3.10"
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Bundled Claude Code CLI version."""
22

3-
__cli_version__ = "2.1.42"
3+
__cli_version__ = "2.1.49"

src/claude_agent_sdk/_internal/client.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,11 @@ async def process_query(
136136
# Stream input in background for async iterables
137137
query._tg.start_soon(query.stream_input, prompt)
138138

139-
# Yield parsed messages
139+
# Yield parsed messages, skipping unknown message types
140140
async for data in query.receive_messages():
141-
yield parse_message(data)
141+
message = parse_message(data)
142+
if message is not None:
143+
yield message
142144

143145
finally:
144146
await query.close()

src/claude_agent_sdk/_internal/message_parser.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
logger = logging.getLogger(__name__)
2222

2323

24-
def parse_message(data: dict[str, Any]) -> Message:
24+
def parse_message(data: dict[str, Any]) -> Message | None:
2525
"""
2626
Parse message from CLI output into typed Message objects.
2727
@@ -178,4 +178,7 @@ def parse_message(data: dict[str, Any]) -> Message:
178178
) from e
179179

180180
case _:
181-
raise MessageParseError(f"Unknown message type: {message_type}", data)
181+
# Forward-compatible: skip unrecognized message types so newer
182+
# CLI versions don't crash older SDK versions.
183+
logger.debug("Skipping unknown message type: %s", message_type)
184+
return None

src/claude_agent_sdk/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Version information for claude-agent-sdk."""
22

3-
__version__ = "0.1.36"
3+
__version__ = "0.1.39"

src/claude_agent_sdk/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,9 @@ async def receive_messages(self) -> AsyncIterator[Message]:
184184
from ._internal.message_parser import parse_message
185185

186186
async for data in self._query.receive_messages():
187-
yield parse_message(data)
187+
message = parse_message(data)
188+
if message is not None:
189+
yield message
188190

189191
async def query(
190192
self, prompt: str | AsyncIterable[dict[str, Any]], session_id: str = "default"

tests/test_message_parser.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -328,10 +328,9 @@ def test_parse_missing_type_field(self):
328328
assert "Message missing 'type' field" in str(exc_info.value)
329329

330330
def test_parse_unknown_message_type(self):
331-
"""Test that unknown message type raises MessageParseError."""
332-
with pytest.raises(MessageParseError) as exc_info:
333-
parse_message({"type": "unknown_type"})
334-
assert "Unknown message type: unknown_type" in str(exc_info.value)
331+
"""Test that unknown message type returns None for forward compatibility."""
332+
result = parse_message({"type": "unknown_type"})
333+
assert result is None
335334

336335
def test_parse_user_message_missing_fields(self):
337336
"""Test that user message with missing fields raises MessageParseError."""
@@ -359,7 +358,8 @@ def test_parse_result_message_missing_fields(self):
359358

360359
def test_message_parse_error_contains_data(self):
361360
"""Test that MessageParseError contains the original data."""
362-
data = {"type": "unknown", "some": "data"}
361+
# Use a malformed known type (missing required fields) to trigger error
362+
data = {"type": "assistant"}
363363
with pytest.raises(MessageParseError) as exc_info:
364364
parse_message(data)
365365
assert exc_info.value.data == data
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Repro test: rate_limit_event message type crashes the Python Agent SDK.
2+
3+
CLI v2.1.45+ emits `rate_limit_event` messages when rate limit status changes
4+
for claude.ai subscription users. The Python SDK's message parser had no handler
5+
for this message type, causing a MessageParseError crash.
6+
7+
Fix: the parser now returns None for unknown message types, and the caller
8+
filters them out. This makes the SDK forward-compatible with new CLI message types.
9+
10+
See: https://github.com/anthropics/claude-agent-sdk-python/issues/583
11+
"""
12+
13+
from claude_agent_sdk._internal.message_parser import parse_message
14+
15+
16+
class TestRateLimitEventHandling:
17+
"""Verify rate_limit_event and unknown message types don't crash."""
18+
19+
def test_rate_limit_event_returns_none(self):
20+
"""rate_limit_event should be silently skipped, not crash."""
21+
data = {
22+
"type": "rate_limit_event",
23+
"rate_limit_info": {
24+
"status": "allowed_warning",
25+
"resetsAt": 1700000000,
26+
"rateLimitType": "five_hour",
27+
"utilization": 0.85,
28+
"isUsingOverage": False,
29+
},
30+
"uuid": "550e8400-e29b-41d4-a716-446655440000",
31+
"session_id": "test-session-id",
32+
}
33+
34+
result = parse_message(data)
35+
assert result is None
36+
37+
def test_rate_limit_event_rejected_returns_none(self):
38+
"""Hard rate limit (status=rejected) should also be skipped."""
39+
data = {
40+
"type": "rate_limit_event",
41+
"rate_limit_info": {
42+
"status": "rejected",
43+
"resetsAt": 1700003600,
44+
"rateLimitType": "seven_day",
45+
"isUsingOverage": False,
46+
"overageStatus": "rejected",
47+
"overageDisabledReason": "out_of_credits",
48+
},
49+
"uuid": "660e8400-e29b-41d4-a716-446655440001",
50+
"session_id": "test-session-id",
51+
}
52+
53+
result = parse_message(data)
54+
assert result is None
55+
56+
def test_unknown_message_type_returns_none(self):
57+
"""Any unknown message type should return None for forward compatibility."""
58+
data = {
59+
"type": "some_future_event_type",
60+
"uuid": "770e8400-e29b-41d4-a716-446655440002",
61+
"session_id": "test-session-id",
62+
}
63+
64+
result = parse_message(data)
65+
assert result is None
66+
67+
def test_known_message_types_still_parsed(self):
68+
"""Known message types should still be parsed normally."""
69+
data = {
70+
"type": "assistant",
71+
"message": {
72+
"content": [{"type": "text", "text": "hello"}],
73+
"model": "claude-sonnet-4-6-20250929",
74+
},
75+
}
76+
77+
result = parse_message(data)
78+
assert result is not None
79+
assert result.content[0].text == "hello"

0 commit comments

Comments
 (0)