clawd-code-sdk 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/PKG-INFO +2 -2
  2. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/README.md +1 -1
  3. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/pyproject.toml +1 -1
  4. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/__init__.py +4 -2
  5. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/_internal/transport/subprocess_cli.py +6 -16
  6. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/client.py +3 -3
  7. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/models/__init__.py +6 -2
  8. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/models/messages.py +24 -27
  9. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/models/options.py +12 -10
  10. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/storage/replay.py +1 -1
  11. clawd_code_sdk-0.2.2/tests/conftest.py +35 -0
  12. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/tests/test_client.py +39 -50
  13. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/tests/test_integration.py +14 -23
  14. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/tests/test_message_parser.py +17 -21
  15. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/tests/test_session.py +8 -10
  16. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/tests/test_streaming_client.py +14 -35
  17. clawd_code_sdk-0.2.1/tests/conftest.py +0 -10
  18. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/.gitignore +0 -0
  19. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/LICENSE +0 -0
  20. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/_bundled/.gitignore +0 -0
  21. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/_errors.py +0 -0
  22. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/_internal/__init__.py +0 -0
  23. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/_internal/message_parser.py +0 -0
  24. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/_internal/query.py +0 -0
  25. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/_internal/transport/__init__.py +0 -0
  26. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/_version.py +0 -0
  27. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/anthropic_types.py +0 -0
  28. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/mcp_utils.py +0 -0
  29. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/models/agents.py +0 -0
  30. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/models/base.py +0 -0
  31. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/models/content_blocks.py +0 -0
  32. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/models/control.py +0 -0
  33. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/models/hooks.py +0 -0
  34. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/models/input_types.py +0 -0
  35. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/models/mcp.py +0 -0
  36. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/models/output_types.py +0 -0
  37. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/models/permissions.py +0 -0
  38. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/models/sandbox.py +0 -0
  39. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/models/server_info.py +0 -0
  40. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/models/ts_output_types.py +0 -0
  41. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/py.typed +0 -0
  42. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/query.py +0 -0
  43. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/session.py +0 -0
  44. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/storage/ARCHITECTURE.md +0 -0
  45. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/storage/__init__.py +0 -0
  46. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/storage/helpers.py +0 -0
  47. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/storage/models.py +0 -0
  48. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/src/clawd_code_sdk/usage.py +0 -0
  49. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/tests/mcp_server.py +0 -0
  50. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/tests/test_changelog.py +0 -0
  51. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/tests/test_errors.py +0 -0
  52. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/tests/test_image.png +0 -0
  53. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/tests/test_sdk_mcp_integration.py +0 -0
  54. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/tests/test_subprocess_buffering.py +0 -0
  55. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/tests/test_tool_callbacks.py +0 -0
  56. {clawd_code_sdk-0.2.1 → clawd_code_sdk-0.2.2}/tests/test_transport.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clawd-code-sdk
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Python SDK for Claude Code
5
5
  Project-URL: Documentation, https://github.com/phil65/claude-agent-sdk-python
6
6
  Project-URL: Homepage, https://github.com/phil65/claude-agent-sdk-python
@@ -271,7 +271,7 @@ async with ClaudeSDKClient(options=options) as client:
271
271
  See [src/clawd_code_sdk/types.py](src/clawd_code_sdk/types.py) for complete type definitions:
272
272
 
273
273
  - `ClaudeAgentOptions` - Configuration options
274
- - `AssistantMessage`, `UserMessage`, `SystemMessage`, `ResultMessage` - Message types
274
+ - `AssistantMessage`, `UserMessage`, `InitSystemMessage`, `ResultMessage` - Message types
275
275
  - `TextBlock`, `ToolUseBlock`, `ToolResultBlock` - Content blocks
276
276
 
277
277
  ## Error Handling
@@ -238,7 +238,7 @@ async with ClaudeSDKClient(options=options) as client:
238
238
  See [src/clawd_code_sdk/types.py](src/clawd_code_sdk/types.py) for complete type definitions:
239
239
 
240
240
  - `ClaudeAgentOptions` - Configuration options
241
- - `AssistantMessage`, `UserMessage`, `SystemMessage`, `ResultMessage` - Message types
241
+ - `AssistantMessage`, `UserMessage`, `InitSystemMessage`, `ResultMessage` - Message types
242
242
  - `TextBlock`, `ToolUseBlock`, `ToolResultBlock` - Content blocks
243
243
 
244
244
  ## Error Handling
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "clawd-code-sdk"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Python SDK for Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -61,7 +61,8 @@ from .models import (
61
61
  SubagentStartHookInput,
62
62
  SubagentStartHookSpecificOutput,
63
63
  SubagentStopHookInput,
64
- SystemMessage,
64
+ BaseSystemMessage,
65
+ InitSystemMessage,
65
66
  TextBlock,
66
67
  ThinkingBlock,
67
68
  ThinkingConfig,
@@ -115,7 +116,8 @@ __all__ = [
115
116
  "UserPromptMessage",
116
117
  "UserPromptMessageContent",
117
118
  "AssistantMessage",
118
- "SystemMessage",
119
+ "BaseSystemMessage",
120
+ "InitSystemMessage",
119
121
  "ResultMessage",
120
122
  "Message",
121
123
  "ClaudeAgentOptions",
@@ -125,30 +125,20 @@ class SubprocessCLITransport(Transport):
125
125
 
126
126
  session = resolve_session_config(self._options.session)
127
127
  match session:
128
- case NewSession(session_id=sid, persist=persist):
129
- if sid is not None:
130
- cmd.extend(["--session-id", sid])
131
- if not persist:
132
- cmd.append("--no-persist-session")
133
- case ResumeSession(
134
- session_id=sid,
135
- fork=fork,
136
- at_message=at_msg,
137
- persist=persist,
138
- ):
128
+ case NewSession(session_id=sid) if sid is not None:
129
+ cmd.extend(["--session-id", sid])
130
+ case ResumeSession(session_id=sid, fork=fork, at_message=at_msg):
139
131
  cmd.extend(["--resume", sid])
140
132
  if fork:
141
133
  cmd.append("--fork-session")
142
134
  if at_msg is not None:
143
135
  cmd.extend(["--resume-session-at", at_msg])
144
- if not persist:
145
- cmd.append("--no-persist-session")
146
- case ContinueLatest(fork=fork, persist=persist):
136
+ case ContinueLatest(fork=fork):
147
137
  cmd.append("--continue")
148
138
  if fork:
149
139
  cmd.append("--fork-session")
150
- if not persist:
151
- cmd.append("--no-persist-session")
140
+ if not session.persist:
141
+ cmd.append("--no-persist-session")
152
142
 
153
143
  # Handle settings and sandbox: merge sandbox into settings if both are provided
154
144
  if settings_value := self._options.build_settings_value():
@@ -128,10 +128,10 @@ class ClaudeSDKClient:
128
128
  # JSON schema for structured output
129
129
  json_schema: dict[str, Any] | None
130
130
  match self.options.output_schema:
131
- case type() as tp:
131
+ case type() as typ:
132
132
  from pydantic import TypeAdapter
133
133
 
134
- json_schema = TypeAdapter(tp).json_schema()
134
+ json_schema = TypeAdapter(typ).json_schema()
135
135
  case dict() as schema:
136
136
  json_schema = schema
137
137
  case None:
@@ -342,7 +342,7 @@ class ClaudeSDKClient:
342
342
  - If no ResultMessage is received, the iterator continues indefinitely
343
343
 
344
344
  Yields:
345
- Message: Each message received (UserMessage, AssistantMessage, SystemMessage, ResultMessage)
345
+ Message: Each message received (UserMessage, AssistantMessage, InitSystemMessage, ResultMessage)
346
346
 
347
347
  Example:
348
348
  ```python
@@ -200,9 +200,11 @@ from .content_blocks import (
200
200
  from .messages import (
201
201
  AssistantMessage,
202
202
  AssistantMessageError,
203
+ BaseSystemMessage,
203
204
  CompactBoundarySystemMessage,
204
205
  HookResponseSystemMessage,
205
206
  HookStartedSystemMessage,
207
+ InitSystemMessage,
206
208
  McpServerStatus,
207
209
  Message,
208
210
  ModelUsage,
@@ -210,7 +212,6 @@ from .messages import (
210
212
  SDKPermissionDenial,
211
213
  StatusSystemMessage,
212
214
  StreamEvent,
213
- SystemMessage,
214
215
  AuthStatusMessage,
215
216
  FilesPersistedSystemMessage,
216
217
  HookProgressSystemMessage,
@@ -229,6 +230,7 @@ from .messages import (
229
230
  )
230
231
  from .options import ClaudeAgentOptions
231
232
  from .options import (
233
+ BaseSessionConfig,
232
234
  ContinueLatest,
233
235
  NewSession,
234
236
  ResumeSession,
@@ -337,10 +339,12 @@ __all__ = [
337
339
  # messages
338
340
  "AssistantMessage",
339
341
  "AssistantMessageError",
342
+ "BaseSystemMessage",
340
343
  "CompactBoundarySystemMessage",
341
344
  "ContentBlock",
342
345
  "HookResponseSystemMessage",
343
346
  "HookStartedSystemMessage",
347
+ "InitSystemMessage",
344
348
  "McpServerStatus",
345
349
  "Message",
346
350
  "ModelUsage",
@@ -348,7 +352,6 @@ __all__ = [
348
352
  "SDKPermissionDenial",
349
353
  "StatusSystemMessage",
350
354
  "StreamEvent",
351
- "SystemMessage",
352
355
  "AuthStatusMessage",
353
356
  "FilesPersistedSystemMessage",
354
357
  "HookProgressSystemMessage",
@@ -371,6 +374,7 @@ __all__ = [
371
374
  "system_message_adapter",
372
375
  # options
373
376
  "ClaudeAgentOptions",
377
+ "BaseSessionConfig",
374
378
  "ContinueLatest",
375
379
  "NewSession",
376
380
  "ResumeSession",
@@ -79,6 +79,13 @@ class BaseMessage:
79
79
  session_id: str
80
80
 
81
81
 
82
+ @dataclass(kw_only=True)
83
+ class BaseSystemMessage(BaseMessage):
84
+ """Base class for all system messages."""
85
+
86
+ type: Literal["system"] = "system"
87
+
88
+
82
89
  @dataclass(kw_only=True)
83
90
  class UserMessage(BaseMessage):
84
91
  """User message."""
@@ -164,10 +171,9 @@ class Plugin:
164
171
 
165
172
 
166
173
  @dataclass(kw_only=True)
167
- class SystemMessage(BaseMessage):
168
- """System message with metadata."""
174
+ class InitSystemMessage(BaseSystemMessage):
175
+ """System init message with session metadata."""
169
176
 
170
- type: Literal["system"] = "system"
171
177
  subtype: Literal["init"] = "init"
172
178
  apiKeySource: ApiKeySource | None = None # noqa: N815
173
179
  cwd: str
@@ -185,10 +191,9 @@ class SystemMessage(BaseMessage):
185
191
 
186
192
 
187
193
  @dataclass(kw_only=True)
188
- class HookStartedSystemMessage(BaseMessage):
189
- """System message with metadata."""
194
+ class HookStartedSystemMessage(BaseSystemMessage):
195
+ """System message emitted when a hook starts."""
190
196
 
191
- type: Literal["system"] = "system"
192
197
  subtype: Literal["hook_started"] = "hook_started"
193
198
  hook_id: str | None = None
194
199
  hook_name: str | None = None
@@ -196,12 +201,11 @@ class HookStartedSystemMessage(BaseMessage):
196
201
 
197
202
 
198
203
  @dataclass(kw_only=True)
199
- class StatusSystemMessage(BaseMessage):
204
+ class StatusSystemMessage(BaseSystemMessage):
200
205
  """System status message."""
201
206
 
202
- type: Literal["system"] = "system"
203
207
  subtype: Literal["status"] = "status"
204
- status: Literal["compacting"] | str | None
208
+ status: Literal["compacting"] | None
205
209
 
206
210
 
207
211
  class TriggerMetadata(TypedDict):
@@ -210,10 +214,9 @@ class TriggerMetadata(TypedDict):
210
214
 
211
215
 
212
216
  @dataclass(kw_only=True)
213
- class CompactBoundarySystemMessage(BaseMessage):
214
- """System message with metadata."""
217
+ class CompactBoundarySystemMessage(BaseSystemMessage):
218
+ """System message emitted at compaction boundaries."""
215
219
 
216
- type: Literal["system"] = "system"
217
220
  subtype: Literal["compact_boundary"] = "compact_boundary"
218
221
  compact_metadata: TriggerMetadata
219
222
 
@@ -237,10 +240,9 @@ class RateLimitMessage(BaseMessage):
237
240
 
238
241
 
239
242
  @dataclass(kw_only=True)
240
- class TaskStartedSystemMessage(BaseMessage):
243
+ class TaskStartedSystemMessage(BaseSystemMessage):
241
244
  """System message emitted when a subagent task starts."""
242
245
 
243
- type: Literal["system"] = "system"
244
246
  subtype: Literal["task_started"] = "task_started"
245
247
  task_id: str = ""
246
248
  tool_use_id: str | None = None
@@ -249,10 +251,9 @@ class TaskStartedSystemMessage(BaseMessage):
249
251
 
250
252
 
251
253
  @dataclass(kw_only=True)
252
- class TaskNotificationSystemMessage(BaseMessage):
254
+ class TaskNotificationSystemMessage(BaseSystemMessage):
253
255
  """System message emitted when a subagent task completes, fails, or stops."""
254
256
 
255
- type: Literal["system"] = "system"
256
257
  subtype: Literal["task_notification"] = "task_notification"
257
258
  task_id: str = ""
258
259
  status: Literal["completed", "failed", "stopped"] = "completed"
@@ -270,10 +271,9 @@ class TaskProgressUsage(TypedDict):
270
271
 
271
272
 
272
273
  @dataclass(kw_only=True)
273
- class TaskProgressSystemMessage(BaseMessage):
274
+ class TaskProgressSystemMessage(BaseSystemMessage):
274
275
  """System message emitted periodically with task progress updates."""
275
276
 
276
- type: Literal["system"] = "system"
277
277
  subtype: Literal["task_progress"] = "task_progress"
278
278
  task_id: str = ""
279
279
  tool_use_id: str | None = None
@@ -297,10 +297,9 @@ class FilePersistedFailure(TypedDict):
297
297
 
298
298
 
299
299
  @dataclass(kw_only=True)
300
- class FilesPersistedSystemMessage(BaseMessage):
300
+ class FilesPersistedSystemMessage(BaseSystemMessage):
301
301
  """System message emitted when files have been persisted."""
302
302
 
303
- type: Literal["system"] = "system"
304
303
  subtype: Literal["files_persisted"] = "files_persisted"
305
304
  files: list[FilePersistedEntry] | None = None
306
305
  failed: list[FilePersistedFailure] | None = None
@@ -310,10 +309,9 @@ class FilesPersistedSystemMessage(BaseMessage):
310
309
 
311
310
 
312
311
  @dataclass(kw_only=True)
313
- class HookProgressSystemMessage(BaseMessage):
312
+ class HookProgressSystemMessage(BaseSystemMessage):
314
313
  """Progress update from a running hook."""
315
314
 
316
- type: Literal["system"] = "system"
317
315
  subtype: Literal["hook_progress"] = "hook_progress"
318
316
  hook_id: str = ""
319
317
  hook_name: str = ""
@@ -324,10 +322,9 @@ class HookProgressSystemMessage(BaseMessage):
324
322
 
325
323
 
326
324
  @dataclass(kw_only=True)
327
- class HookResponseSystemMessage(BaseMessage):
328
- """System message with metadata."""
325
+ class HookResponseSystemMessage(BaseSystemMessage):
326
+ """System message emitted when a hook completes."""
329
327
 
330
- type: Literal["system"] = "system"
331
328
  subtype: Literal["hook_response"] = "hook_response"
332
329
  hook_id: str
333
330
  hook_name: str
@@ -441,7 +438,7 @@ class AuthStatusMessage(BaseMessage):
441
438
 
442
439
 
443
440
  SystemMessageUnion = Annotated[
444
- SystemMessage
441
+ InitSystemMessage
445
442
  | HookStartedSystemMessage
446
443
  | StatusSystemMessage
447
444
  | CompactBoundarySystemMessage
@@ -460,7 +457,7 @@ system_message_adapter: TypeAdapter[SystemMessageUnion] = TypeAdapter(SystemMess
460
457
  Message = (
461
458
  UserMessage
462
459
  | AssistantMessage
463
- | SystemMessage
460
+ | InitSystemMessage
464
461
  | ResultMessage
465
462
  | StreamEvent
466
463
  | RateLimitMessage
@@ -30,49 +30,51 @@ logger = logging.getLogger(__name__)
30
30
 
31
31
 
32
32
  @dataclass(kw_only=True)
33
- class NewSession:
33
+ class BaseSessionConfig:
34
+ """Common fields for all session configurations."""
35
+
36
+ persist: bool = True
37
+ """Whether to persist the session to disk."""
38
+
39
+
40
+ @dataclass(kw_only=True)
41
+ class NewSession(BaseSessionConfig):
34
42
  """Start a fresh session.
35
43
 
36
44
  Attributes:
37
45
  session_id: Deterministic session ID, or None to auto-generate a UUID.
38
- persist: Whether to persist the session to disk.
39
46
  """
40
47
 
41
48
  mode: Literal["new"] = "new"
42
49
  session_id: str | None = None
43
- persist: bool = True
44
50
 
45
51
 
46
52
  @dataclass(kw_only=True)
47
- class ResumeSession:
53
+ class ResumeSession(BaseSessionConfig):
48
54
  """Resume an existing session by ID.
49
55
 
50
56
  Attributes:
51
57
  session_id: The session ID to resume.
52
58
  fork: If True, fork to a new session ID instead of continuing in-place.
53
59
  at_message: Resume from a specific message UUID within the session.
54
- persist: Whether to persist the session to disk.
55
60
  """
56
61
 
57
62
  mode: Literal["resume"] = "resume"
58
63
  session_id: str
59
64
  fork: bool = False
60
65
  at_message: str | None = None
61
- persist: bool = True
62
66
 
63
67
 
64
68
  @dataclass(kw_only=True)
65
- class ContinueLatest:
69
+ class ContinueLatest(BaseSessionConfig):
66
70
  """Continue the most recent conversation.
67
71
 
68
72
  Attributes:
69
73
  fork: If True, fork to a new session ID instead of continuing in-place.
70
- persist: Whether to persist the session to disk.
71
74
  """
72
75
 
73
76
  mode: Literal["continue"] = "continue"
74
77
  fork: bool = False
75
- persist: bool = True
76
78
 
77
79
 
78
80
  SessionConfig = NewSession | ResumeSession | ContinueLatest
@@ -98,7 +100,7 @@ def resolve_session_config(value: str | SessionConfig | None) -> SessionConfig:
98
100
  return NewSession()
99
101
  case str() as session_id:
100
102
  return ResumeSession(session_id=session_id)
101
- case NewSession() | ResumeSession() | ContinueLatest() as config:
103
+ case BaseSessionConfig() as config:
102
104
  return config
103
105
 
104
106
 
@@ -17,7 +17,7 @@ full content (since token-level deltas are not preserved in storage).
17
17
 
18
18
  Not reconstructible from storage:
19
19
  - Token-level streaming granularity (deltas are synthetic, one per block).
20
- - SystemMessage(init): Session initialization metadata (tools, model, etc.).
20
+ - InitSystemMessage(init): Session initialization metadata (tools, model, etc.).
21
21
  - RateLimitMessage: Rate limit events are transient.
22
22
 
23
23
  Partially reconstructible (with ``include_result=True``):
@@ -0,0 +1,35 @@
1
+ """Pytest configuration for tests."""
2
+
3
+ import os
4
+ from typing import Any
5
+
6
+ import pytest
7
+
8
+
9
+ _MSG_COUNTER = 0
10
+
11
+
12
+ def make_beta_message(
13
+ content: list[dict[str, Any]],
14
+ model: str = "claude-opus-4-1-20250805",
15
+ **kwargs: Any,
16
+ ) -> dict[str, Any]:
17
+ """Create a wire-format message dict matching BetaMessage shape."""
18
+ global _MSG_COUNTER # noqa: PLW0603
19
+ _MSG_COUNTER += 1
20
+ return {
21
+ "id": f"msg_test_{_MSG_COUNTER:04d}",
22
+ "type": "message",
23
+ "role": "assistant",
24
+ "content": content,
25
+ "model": model,
26
+ "stop_reason": "end_turn",
27
+ "stop_sequence": None,
28
+ "usage": {"input_tokens": 10, "output_tokens": 5},
29
+ **kwargs,
30
+ }
31
+
32
+
33
+ @pytest.fixture(scope="session", autouse=True)
34
+ def unset_anthropic_api_key():
35
+ os.environ["ANTHROPIC_API_KEY"] = ""
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Literal
9
9
  from unittest.mock import AsyncMock
10
10
 
11
11
  import anyio
12
+ from conftest import make_beta_message
12
13
  import pytest
13
14
 
14
15
  from clawd_code_sdk import (
@@ -130,11 +131,9 @@ class TestQueryFunction:
130
131
  test_messages = [
131
132
  {
132
133
  "type": "assistant",
133
- "message": {
134
- "role": "assistant",
135
- "content": [{"type": "text", "text": "4"}],
136
- "model": "claude-opus-4-1-20250805",
137
- },
134
+ "message": make_beta_message(
135
+ content=[{"type": "text", "text": "4"}],
136
+ ),
138
137
  },
139
138
  _make_result(),
140
139
  ]
@@ -158,11 +157,9 @@ class TestQueryFunction:
158
157
  test_messages = [
159
158
  {
160
159
  "type": "assistant",
161
- "message": {
162
- "role": "assistant",
163
- "content": [{"type": "text", "text": "Hello!"}],
164
- "model": "claude-opus-4-1-20250805",
165
- },
160
+ "message": make_beta_message(
161
+ content=[{"type": "text", "text": "Hello!"}],
162
+ ),
166
163
  },
167
164
  _make_result(),
168
165
  ]
@@ -193,11 +190,9 @@ class TestQueryFunction:
193
190
  test_messages = [
194
191
  {
195
192
  "type": "assistant",
196
- "message": {
197
- "role": "assistant",
198
- "content": [{"type": "text", "text": "Done"}],
199
- "model": "claude-opus-4-1-20250805",
200
- },
193
+ "message": make_beta_message(
194
+ content=[{"type": "text", "text": "Done"}],
195
+ ),
201
196
  },
202
197
  _make_result(),
203
198
  ]
@@ -225,17 +220,16 @@ class TestAPIErrorRaising:
225
220
  async def _test():
226
221
  error_message = {
227
222
  "type": "assistant",
228
- "message": {
229
- "role": "assistant",
230
- "content": [
223
+ "message": make_beta_message(
224
+ content=[
231
225
  {
232
226
  "type": "text",
233
227
  "text": "API Error: The provided model identifier is invalid.",
234
228
  }
235
229
  ],
236
- "model": "claude-invalid-model",
237
- "error": "invalid_request",
238
- },
230
+ model="claude-invalid-model",
231
+ error="invalid_request",
232
+ ),
239
233
  }
240
234
  mock_transport = create_mock_transport_with_messages([error_message])
241
235
 
@@ -255,17 +249,16 @@ class TestAPIErrorRaising:
255
249
  async def _test():
256
250
  error_message = {
257
251
  "type": "assistant",
258
- "message": {
259
- "role": "assistant",
260
- "content": [
252
+ "message": make_beta_message(
253
+ content=[
261
254
  {
262
255
  "type": "text",
263
256
  "text": "API Error: Rate limit exceeded",
264
257
  }
265
258
  ],
266
- "model": "claude-sonnet-4-5-20250514",
267
- "error": "rate_limit",
268
- },
259
+ model="claude-sonnet-4-5-20250514",
260
+ error="rate_limit",
261
+ ),
269
262
  }
270
263
  mock_transport = create_mock_transport_with_messages([error_message])
271
264
 
@@ -283,12 +276,11 @@ class TestAPIErrorRaising:
283
276
  async def _test():
284
277
  error_message = {
285
278
  "type": "assistant",
286
- "message": {
287
- "role": "assistant",
288
- "content": [{"type": "text", "text": "API Error: Invalid API key"}],
289
- "model": "claude-sonnet-4-5-20250514",
290
- "error": "authentication_failed",
291
- },
279
+ "message": make_beta_message(
280
+ content=[{"type": "text", "text": "API Error: Invalid API key"}],
281
+ model="claude-sonnet-4-5-20250514",
282
+ error="authentication_failed",
283
+ ),
292
284
  }
293
285
  mock_transport = create_mock_transport_with_messages([error_message])
294
286
 
@@ -306,17 +298,16 @@ class TestAPIErrorRaising:
306
298
  async def _test():
307
299
  error_message = {
308
300
  "type": "assistant",
309
- "message": {
310
- "role": "assistant",
311
- "content": [
301
+ "message": make_beta_message(
302
+ content=[
312
303
  {
313
304
  "type": "text",
314
305
  "text": "API Error: Repeated 529 Overloaded errors",
315
306
  }
316
307
  ],
317
- "model": "claude-sonnet-4-5-20250514",
318
- "error": "server_error",
319
- },
308
+ model="claude-sonnet-4-5-20250514",
309
+ error="server_error",
310
+ ),
320
311
  }
321
312
  mock_transport = create_mock_transport_with_messages([error_message])
322
313
 
@@ -334,12 +325,11 @@ class TestAPIErrorRaising:
334
325
  async def _test():
335
326
  error_message = {
336
327
  "type": "assistant",
337
- "message": {
338
- "role": "assistant",
339
- "content": [{"type": "text", "text": "Unknown error"}],
340
- "model": "claude-sonnet-4-5-20250514",
341
- "error": "unknown",
342
- },
328
+ "message": make_beta_message(
329
+ content=[{"type": "text", "text": "Unknown error"}],
330
+ model="claude-sonnet-4-5-20250514",
331
+ error="unknown",
332
+ ),
343
333
  }
344
334
  mock_transport = create_mock_transport_with_messages([error_message])
345
335
 
@@ -358,11 +348,10 @@ class TestAPIErrorRaising:
358
348
  test_messages = [
359
349
  {
360
350
  "type": "assistant",
361
- "message": {
362
- "role": "assistant",
363
- "content": [{"type": "text", "text": "Hello!"}],
364
- "model": "claude-sonnet-4-5-20250514",
365
- },
351
+ "message": make_beta_message(
352
+ content=[{"type": "text", "text": "Hello!"}],
353
+ model="claude-sonnet-4-5-20250514",
354
+ ),
366
355
  },
367
356
  _make_result(uuid="msg-002"),
368
357
  ]
@@ -8,6 +8,7 @@ import json
8
8
  from unittest.mock import AsyncMock, patch
9
9
 
10
10
  import anyio
11
+ from conftest import make_beta_message
11
12
  import pytest
12
13
 
13
14
  from clawd_code_sdk import (
@@ -74,11 +75,9 @@ class TestIntegration:
74
75
  test_messages = [
75
76
  {
76
77
  "type": "assistant",
77
- "message": {
78
- "role": "assistant",
79
- "content": [{"type": "text", "text": "2 + 2 equals 4"}],
80
- "model": "claude-opus-4-1-20250805",
81
- },
78
+ "message": make_beta_message(
79
+ content=[{"type": "text", "text": "2 + 2 equals 4"}]
80
+ ),
82
81
  },
83
82
  {
84
83
  "type": "result",
@@ -121,9 +120,8 @@ class TestIntegration:
121
120
  test_messages = [
122
121
  {
123
122
  "type": "assistant",
124
- "message": {
125
- "role": "assistant",
126
- "content": [
123
+ "message": make_beta_message(
124
+ content=[
127
125
  {"type": "text", "text": "Let me read that file for you."},
128
126
  {
129
127
  "type": "tool_use",
@@ -131,9 +129,8 @@ class TestIntegration:
131
129
  "name": "Read",
132
130
  "input": {"file_path": "/test.txt"},
133
131
  },
134
- ],
135
- "model": "claude-opus-4-1-20250805",
136
- },
132
+ ]
133
+ ),
137
134
  },
138
135
  {
139
136
  "type": "result",
@@ -196,13 +193,9 @@ class TestIntegration:
196
193
  test_messages = [
197
194
  {
198
195
  "type": "assistant",
199
- "message": {
200
- "role": "assistant",
201
- "content": [
202
- {"type": "text", "text": "Continuing from previous conversation"}
203
- ],
204
- "model": "claude-opus-4-1-20250805",
205
- },
196
+ "message": make_beta_message(
197
+ content=[{"type": "text", "text": "Continuing from previous conversation"}]
198
+ ),
206
199
  },
207
200
  {
208
201
  "type": "result",
@@ -245,11 +238,9 @@ class TestIntegration:
245
238
  test_messages = [
246
239
  {
247
240
  "type": "assistant",
248
- "message": {
249
- "role": "assistant",
250
- "content": [{"type": "text", "text": "Starting to read..."}],
251
- "model": "claude-opus-4-1-20250805",
252
- },
241
+ "message": make_beta_message(
242
+ content=[{"type": "text", "text": "Starting to read..."}]
243
+ ),
253
244
  },
254
245
  {
255
246
  "type": "result",
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from conftest import make_beta_message
5
6
  import pytest
6
7
 
7
8
  from clawd_code_sdk._errors import MessageParseError
@@ -56,13 +57,12 @@ class TestContentBlockDispatch:
56
57
  """Thinking blocks parse with signature preserved."""
57
58
  data = {
58
59
  "type": "assistant",
59
- "message": {
60
- "content": [
60
+ "message": make_beta_message(
61
+ content=[
61
62
  {"type": "thinking", "thinking": "hmm...", "signature": "sig-abc"},
62
63
  {"type": "text", "text": "answer"},
63
64
  ],
64
- "model": "claude-opus-4-1-20250805",
65
- },
65
+ ),
66
66
  }
67
67
  msg = parse_message(data)
68
68
  assert isinstance(msg, AssistantMessage)
@@ -100,10 +100,9 @@ class TestParentToolUseId:
100
100
  def test_assistant_message(self):
101
101
  data = {
102
102
  "type": "assistant",
103
- "message": {
104
- "content": [{"type": "text", "text": "hi"}],
105
- "model": "claude-opus-4-1-20250805",
106
- },
103
+ "message": make_beta_message(
104
+ content=[{"type": "text", "text": "hi"}],
105
+ ),
107
106
  "parent_tool_use_id": "toolu_xyz",
108
107
  }
109
108
  msg = parse_message(data)
@@ -116,11 +115,10 @@ class TestAssistantErrorExtraction:
116
115
  def test_error_from_message_level(self):
117
116
  data = {
118
117
  "type": "assistant",
119
- "message": {
120
- "content": [{"type": "text", "text": "API Error: bad key"}],
121
- "model": "claude-opus-4-1-20250805",
122
- "error": "authentication_failed",
123
- },
118
+ "message": make_beta_message(
119
+ content=[{"type": "text", "text": "API Error: bad key"}],
120
+ error="authentication_failed",
121
+ ),
124
122
  }
125
123
  msg = parse_message(data)
126
124
  assert isinstance(msg, AssistantMessage) and msg.error == "authentication_failed"
@@ -129,10 +127,9 @@ class TestAssistantErrorExtraction:
129
127
  data = {
130
128
  "type": "assistant",
131
129
  "error": "rate_limit",
132
- "message": {
133
- "content": [{"type": "text", "text": "Rate limited"}],
134
- "model": "claude-opus-4-1-20250805",
135
- },
130
+ "message": make_beta_message(
131
+ content=[{"type": "text", "text": "Rate limited"}],
132
+ ),
136
133
  }
137
134
  msg = parse_message(data)
138
135
  assert isinstance(msg, AssistantMessage) and msg.error == "rate_limit"
@@ -140,10 +137,9 @@ class TestAssistantErrorExtraction:
140
137
  def test_no_error(self):
141
138
  data = {
142
139
  "type": "assistant",
143
- "message": {
144
- "content": [{"type": "text", "text": "ok"}],
145
- "model": "claude-opus-4-1-20250805",
146
- },
140
+ "message": make_beta_message(
141
+ content=[{"type": "text", "text": "ok"}],
142
+ ),
147
143
  }
148
144
  msg = parse_message(data)
149
145
  assert isinstance(msg, AssistantMessage) and msg.error is None
@@ -8,6 +8,7 @@ import json
8
8
  from unittest.mock import AsyncMock
9
9
 
10
10
  import anyio
11
+ from conftest import make_beta_message
11
12
  import pytest
12
13
 
13
14
  from clawd_code_sdk import (
@@ -73,18 +74,15 @@ def _create_mock_transport_with_messages(messages: list[dict]) -> AsyncMock:
73
74
 
74
75
  ASSISTANT_MSG = {
75
76
  "type": "assistant",
76
- "message": {
77
- "role": "assistant",
78
- "content": [{"type": "text", "text": "Hello, world!"}],
79
- "model": "claude-sonnet-4-5-20250514",
80
- },
77
+ "message": make_beta_message(
78
+ content=[{"type": "text", "text": "Hello, world!"}], model="claude-sonnet-4-5-20250514"
79
+ ),
81
80
  }
82
81
 
83
82
  TOOL_USE_MSG = {
84
83
  "type": "assistant",
85
- "message": {
86
- "role": "assistant",
87
- "content": [
84
+ "message": make_beta_message(
85
+ content=[
88
86
  {"type": "text", "text": "Let me read that."},
89
87
  {
90
88
  "type": "tool_use",
@@ -93,8 +91,8 @@ TOOL_USE_MSG = {
93
91
  "input": {"path": "/tmp/test.py"},
94
92
  },
95
93
  ],
96
- "model": "claude-sonnet-4-5-20250514",
97
- },
94
+ model="claude-sonnet-4-5-20250514",
95
+ ),
98
96
  }
99
97
 
100
98
  RESULT_MSG = asdict(
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING
11
11
  from unittest.mock import AsyncMock, patch
12
12
 
13
13
  import anyio
14
+ from conftest import make_beta_message
14
15
  import pytest
15
16
 
16
17
  from clawd_code_sdk import (
@@ -324,11 +325,7 @@ class TestClaudeSDKClientStreaming:
324
325
  # Then yield the actual messages
325
326
  yield {
326
327
  "type": "assistant",
327
- "message": {
328
- "role": "assistant",
329
- "content": [{"type": "text", "text": "Hello!"}],
330
- "model": "claude-opus-4-1-20250805",
331
- },
328
+ "message": make_beta_message(content=[{"type": "text", "text": "Hello!"}]),
332
329
  }
333
330
  yield {
334
331
  "type": "user",
@@ -394,11 +391,7 @@ class TestClaudeSDKClientStreaming:
394
391
  # Then yield the actual messages
395
392
  yield {
396
393
  "type": "assistant",
397
- "message": {
398
- "role": "assistant",
399
- "content": [{"type": "text", "text": "Answer"}],
400
- "model": "claude-opus-4-1-20250805",
401
- },
394
+ "message": make_beta_message(content=[{"type": "text", "text": "Answer"}]),
402
395
  }
403
396
  yield {
404
397
  "type": "result",
@@ -420,11 +413,9 @@ class TestClaudeSDKClientStreaming:
420
413
  # This should not be yielded
421
414
  yield {
422
415
  "type": "assistant",
423
- "message": {
424
- "role": "assistant",
425
- "content": [{"type": "text", "text": "Should not see this"}],
426
- },
427
- "model": "claude-opus-4-1-20250805",
416
+ "message": make_beta_message(
417
+ content=[{"type": "text", "text": "Should not see this"}]
418
+ ),
428
419
  }
429
420
 
430
421
  mock_transport.read_messages = mock_receive
@@ -549,11 +540,9 @@ class TestClaudeSDKClientStreaming:
549
540
  await asyncio.sleep(0.1)
550
541
  yield {
551
542
  "type": "assistant",
552
- "message": {
553
- "role": "assistant",
554
- "content": [{"type": "text", "text": "Response 1"}],
555
- "model": "claude-opus-4-1-20250805",
556
- },
543
+ "message": make_beta_message(
544
+ content=[{"type": "text", "text": "Response 1"}]
545
+ ),
557
546
  }
558
547
  await asyncio.sleep(0.1)
559
548
  yield {
@@ -811,19 +800,11 @@ class TestClaudeSDKClientEdgeCases:
811
800
  # Then yield the actual messages
812
801
  yield {
813
802
  "type": "assistant",
814
- "message": {
815
- "role": "assistant",
816
- "content": [{"type": "text", "text": "Hello"}],
817
- "model": "claude-opus-4-1-20250805",
818
- },
803
+ "message": make_beta_message(content=[{"type": "text", "text": "Hello"}]),
819
804
  }
820
805
  yield {
821
806
  "type": "assistant",
822
- "message": {
823
- "role": "assistant",
824
- "content": [{"type": "text", "text": "World"}],
825
- "model": "claude-opus-4-1-20250805",
826
- },
807
+ "message": make_beta_message(content=[{"type": "text", "text": "World"}]),
827
808
  }
828
809
  yield {
829
810
  "type": "result",
@@ -911,11 +892,9 @@ class TestAsyncGeneratorCleanup:
911
892
  for i in range(5):
912
893
  yield {
913
894
  "type": "assistant",
914
- "message": {
915
- "role": "assistant",
916
- "content": [{"type": "text", "text": f"Message {i}"}],
917
- "model": "claude-opus-4-1-20250805",
918
- },
895
+ "message": make_beta_message(
896
+ content=[{"type": "text", "text": f"Message {i}"}]
897
+ ),
919
898
  }
920
899
 
921
900
  mock_transport.read_messages = mock_receive
@@ -1,10 +0,0 @@
1
- """Pytest configuration for tests."""
2
-
3
- import os
4
-
5
- import pytest
6
-
7
-
8
- @pytest.fixture(scope="session", autouse=True)
9
- def unset_anthropic_api_key():
10
- os.environ["ANTHROPIC_API_KEY"] = ""
File without changes