clawd-code-sdk 0.3.8__tar.gz → 0.4.0__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 (71) hide show
  1. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/.gitignore +1 -1
  2. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/PKG-INFO +2 -1
  3. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/pyproject.toml +2 -1
  4. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/__init__.py +14 -4
  5. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_internal/query.py +1 -35
  6. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_internal/transport/subprocess_cli.py +9 -7
  7. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/client.py +52 -47
  8. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/__init__.py +10 -14
  9. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/base.py +7 -7
  10. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/messages.py +79 -22
  11. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/options.py +10 -21
  12. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/query.py +9 -8
  13. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/session.py +3 -1
  14. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_agents_and_settings.py +2 -2
  15. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_include_partial_messages.py +11 -25
  16. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_slash_commands.py +1 -1
  17. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_stderr_callback.py +2 -2
  18. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_structured_output.py +4 -4
  19. clawd_code_sdk-0.4.0/tests/mock_claude_server.py +92 -0
  20. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_client.py +6 -6
  21. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_integration.py +1 -1
  22. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_streaming_client.py +10 -50
  23. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_subprocess_buffering.py +8 -8
  24. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_transport.py +56 -66
  25. clawd_code_sdk-0.3.8/tests/mock_claude_server.py +0 -92
  26. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/LICENSE +0 -0
  27. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/README.md +0 -0
  28. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_bundled/.gitignore +0 -0
  29. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_errors.py +0 -0
  30. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_internal/__init__.py +0 -0
  31. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_internal/message_parser.py +0 -0
  32. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_internal/transport/__init__.py +0 -0
  33. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_version.py +0 -0
  34. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/anthropic_types.py +0 -0
  35. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/list_sessions.py +0 -0
  36. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/mcp_utils.py +0 -0
  37. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/agents.py +0 -0
  38. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/content_blocks.py +0 -0
  39. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/control.py +0 -0
  40. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/hooks.py +0 -0
  41. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/input_types.py +0 -0
  42. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/mcp.py +0 -0
  43. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/output_types.py +0 -0
  44. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/permissions.py +0 -0
  45. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/sandbox.py +0 -0
  46. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/server_info.py +0 -0
  47. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/py.typed +0 -0
  48. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/storage/ARCHITECTURE.md +0 -0
  49. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/storage/__init__.py +0 -0
  50. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/storage/helpers.py +0 -0
  51. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/storage/models.py +0 -0
  52. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/storage/replay.py +0 -0
  53. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/usage.py +0 -0
  54. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/__init__.py +0 -0
  55. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/conftest.py +0 -0
  56. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/__init__.py +0 -0
  57. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_dynamic_control.py +0 -0
  58. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_hook_events.py +0 -0
  59. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_hooks.py +0 -0
  60. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_mcp_tools.py +0 -0
  61. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_sdk_mcp_tools.py +0 -0
  62. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_subagent_invocation.py +0 -0
  63. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_tool_permissions.py +0 -0
  64. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/mcp_server.py +0 -0
  65. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_changelog.py +0 -0
  66. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_errors.py +0 -0
  67. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_image.png +0 -0
  68. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_message_parser.py +0 -0
  69. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_sdk_mcp_integration.py +0 -0
  70. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_session.py +0 -0
  71. {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_tool_callbacks.py +0 -0
@@ -27,7 +27,7 @@ ENV/
27
27
  env/
28
28
  .venv
29
29
  uv.lock
30
-
30
+ .env
31
31
  # IDEs
32
32
  .vscode/
33
33
  .idea/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clawd-code-sdk
3
- Version: 0.3.8
3
+ Version: 0.4.0
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
@@ -21,6 +21,7 @@ Requires-Dist: anthropic>=0.77.0
21
21
  Requires-Dist: anyenv>=2.0.15
22
22
  Requires-Dist: anyio>=4.0.0
23
23
  Requires-Dist: mcp>=0.1.0
24
+ Requires-Dist: python-dotenv>=1.2.1
24
25
  Provides-Extra: dev
25
26
  Requires-Dist: anyio[trio]>=4.0.0; extra == 'dev'
26
27
  Requires-Dist: mypy>=1.0.0; extra == 'dev'
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "clawd-code-sdk"
3
- version = "0.3.8"
3
+ version = "0.4.0"
4
4
  description = "Python SDK for Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.13"
@@ -23,6 +23,7 @@ dependencies = [
23
23
  "anyenv>=2.0.15",
24
24
  "anyio>=4.0.0",
25
25
  "mcp>=0.1.0",
26
+ "python-dotenv>=1.2.1",
26
27
  ]
27
28
 
28
29
  [project.urls]
@@ -2,6 +2,10 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from dotenv import load_dotenv
6
+
7
+ load_dotenv()
8
+
5
9
 
6
10
  from ._errors import (
7
11
  APIError,
@@ -22,6 +26,7 @@ from .anthropic_types import ToolResultContentBlock
22
26
  from .client import ClaudeSDKClient
23
27
  from .list_sessions import list_sessions
24
28
  from .models import (
29
+ AccumulatedUsage,
25
30
  AgentDefinition,
26
31
  AgentHookHandler,
27
32
  AgentHooksConfig,
@@ -85,9 +90,11 @@ from .models import (
85
90
  ToolResultBlock,
86
91
  ToolUseBlock,
87
92
  UserMessage,
88
- UserPromptMessage,
89
- UserPromptMessageContent,
90
93
  UserPromptSubmitHookInput,
94
+ UserTextPrompt,
95
+ UserImagePrompt,
96
+ UserPrompt,
97
+ ImageMediaType,
91
98
  FromPR,
92
99
  NewSession,
93
100
  ResumeSession,
@@ -113,6 +120,7 @@ __cli_version__ = "2.1.11"
113
120
  __all__ = [
114
121
  # API Errors
115
122
  "APIError",
123
+ "AccumulatedUsage",
116
124
  # Agent support
117
125
  "AgentDefinition",
118
126
  # Hook support
@@ -146,6 +154,7 @@ __all__ = [
146
154
  "HookJSONOutput",
147
155
  "HookMatcher",
148
156
  "HookMatcherConfig",
157
+ "ImageMediaType",
149
158
  "InitSystemMessage",
150
159
  "InvalidRequestError",
151
160
  "ListSessionsOptions",
@@ -212,10 +221,11 @@ __all__ = [
212
221
  "ToolUseBlock",
213
222
  # Transport
214
223
  "Transport",
224
+ "UserImagePrompt",
215
225
  "UserMessage",
216
- "UserPromptMessage",
217
- "UserPromptMessageContent",
226
+ "UserPrompt",
218
227
  "UserPromptSubmitHookInput",
228
+ "UserTextPrompt",
219
229
  "__version__",
220
230
  # MCP Server Support
221
231
  "create_sdk_mcp_server",
@@ -33,7 +33,7 @@ from clawd_code_sdk.models.server_info import ClaudeCodeServerInfo
33
33
 
34
34
 
35
35
  if TYPE_CHECKING:
36
- from collections.abc import AsyncGenerator, AsyncIterable, AsyncIterator, Iterator
36
+ from collections.abc import AsyncGenerator, AsyncIterator, Iterator
37
37
 
38
38
  from anyio.abc import CancelScope, TaskGroup
39
39
  from mcp.server import Server as McpServer
@@ -45,7 +45,6 @@ if TYPE_CHECKING:
45
45
  from clawd_code_sdk.models.hooks import HookCallback, HookEvent, HookMatcher
46
46
  from clawd_code_sdk.models.input_types import AskUserQuestionInput
47
47
  from clawd_code_sdk.models.mcp import JSONRPCMessage, JSONRPCResponse, RequestId
48
- from clawd_code_sdk.models.messages import UserPromptMessage
49
48
  from clawd_code_sdk.models.permissions import CanUseTool, OnUserQuestion, PermissionResult
50
49
 
51
50
  logger = logging.getLogger(__name__)
@@ -473,39 +472,6 @@ class Query:
473
472
  req = {"subtype": "rewind_files", "user_message_id": user_message_id}
474
473
  return await self._send_control_request(req)
475
474
 
476
- async def stream_input(self, stream: AsyncIterable[UserPromptMessage]) -> None:
477
- """Stream input messages to transport.
478
-
479
- If SDK MCP servers or hooks are present, waits for the first result
480
- before closing stdin to allow bidirectional control protocol communication.
481
- """
482
- try:
483
- async for message in stream:
484
- if self._closed:
485
- break
486
- await self.transport.write(anyenv.dump_json(message) + "\n")
487
-
488
- # If we have SDK MCP servers or hooks that need bidirectional communication,
489
- # wait for first result before closing the channel
490
- if self.sdk_mcp_servers or self.hooks:
491
- logger.debug(
492
- "Waiting for first result before closing stdin "
493
- "(sdk_mcp_servers=%s, has_hooks=%s)",
494
- len(self.sdk_mcp_servers),
495
- bool(self.hooks),
496
- )
497
- try:
498
- with anyio.move_on_after(self._stream_close_timeout):
499
- await self._first_result_event.wait()
500
- logger.debug("Received first result, closing input stream")
501
- except Exception:
502
- logger.debug("Timed out waiting for first result, closing input stream")
503
-
504
- # After all messages sent (and result received if needed), end input
505
- await self.transport.end_input()
506
- except Exception as e:
507
- logger.debug("Error streaming input: %s", e)
508
-
509
475
  async def receive_messages(self) -> AsyncGenerator[dict[str, Any]]:
510
476
  """Receive SDK messages (not control messages)."""
511
477
  async for message in self._message_receive:
@@ -26,15 +26,19 @@ from clawd_code_sdk._errors import (
26
26
  )
27
27
  from clawd_code_sdk._internal.transport import Transport
28
28
  from clawd_code_sdk._version import __version__
29
+ from clawd_code_sdk.models.base import (
30
+ ThinkingConfigAdaptive,
31
+ ThinkingConfigDisabled,
32
+ ThinkingConfigEnabled,
33
+ )
29
34
 
30
35
 
31
36
  if TYPE_CHECKING:
32
- from collections.abc import AsyncIterable, AsyncIterator
37
+ from collections.abc import AsyncIterator
33
38
 
34
39
  from anyio.abc import Process
35
40
 
36
41
  from clawd_code_sdk.models import ClaudeAgentOptions
37
- from clawd_code_sdk.models.messages import UserPromptMessage
38
42
 
39
43
  logger = logging.getLogger(__name__)
40
44
 
@@ -56,10 +60,8 @@ class SubprocessCLITransport(Transport):
56
60
 
57
61
  def __init__(
58
62
  self,
59
- prompt: str | AsyncIterable[UserPromptMessage],
60
63
  options: ClaudeAgentOptions,
61
64
  ):
62
- self._prompt = prompt
63
65
  self._options = options
64
66
  self._cli_path = str(options.cli_path) if options.cli_path is not None else _find_cli()
65
67
  self._cwd = str(options.cwd) if options.cwd else None
@@ -207,11 +209,11 @@ class SubprocessCLITransport(Transport):
207
209
 
208
210
  # Resolve thinking config → --max-thinking-tokens
209
211
  match self._options.thinking:
210
- case {"type": "adaptive"}:
212
+ case ThinkingConfigAdaptive():
211
213
  cmd.extend(["--max-thinking-tokens", "32000"])
212
- case {"type": "enabled", "budget_tokens": budget}:
214
+ case ThinkingConfigEnabled(budget_tokens=budget):
213
215
  cmd.extend(["--max-thinking-tokens", str(budget)])
214
- case {"type": "disabled"}:
216
+ case ThinkingConfigDisabled():
215
217
  cmd.extend(["--max-thinking-tokens", "0"])
216
218
 
217
219
  if self._options.effort is not None:
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from collections.abc import AsyncIterable
6
5
  from dataclasses import replace
7
6
  import os
8
7
  from typing import TYPE_CHECKING, Any, Self
@@ -12,12 +11,17 @@ from pydantic import TypeAdapter
12
11
 
13
12
  from clawd_code_sdk._errors import CLIConnectionError
14
13
  from clawd_code_sdk.models import (
14
+ AccumulatedUsage,
15
15
  ClaudeAgentOptions,
16
16
  ResultMessage,
17
- UserPromptMessage,
18
17
  )
19
18
  from clawd_code_sdk.models.mcp import McpStatusResponse
20
- from clawd_code_sdk.models.messages import AssistantMessage
19
+ from clawd_code_sdk.models.messages import (
20
+ AssistantMessage,
21
+ ResultErrorMessage,
22
+ ResultSuccessMessage,
23
+ UserTextPrompt,
24
+ )
21
25
 
22
26
 
23
27
  if TYPE_CHECKING:
@@ -27,6 +31,9 @@ if TYPE_CHECKING:
27
31
  from clawd_code_sdk._internal.query import Query
28
32
  from clawd_code_sdk.models import Message, PermissionMode
29
33
  from clawd_code_sdk.models.mcp import McpServerConfig
34
+ from clawd_code_sdk.models.messages import (
35
+ UserImagePrompt,
36
+ )
30
37
  from clawd_code_sdk.models.server_info import ClaudeCodeServerInfo
31
38
 
32
39
 
@@ -59,6 +66,10 @@ class ClaudeSDKClient:
59
66
  self._custom_transport = transport
60
67
  self._transport: Transport | None = None
61
68
  self._query: Query | None = None
69
+ self.session_usage: AccumulatedUsage = AccumulatedUsage()
70
+ """Cumulative token usage across all queries in this session."""
71
+ self.query_usage: AccumulatedUsage = AccumulatedUsage()
72
+ """Token usage for the current/last query only (reset on each query() call)."""
62
73
  os.environ["CLAUDE_CODE_ENTRYPOINT"] = "sdk-py-client"
63
74
 
64
75
  def _ensure_connected(self) -> Query:
@@ -67,38 +78,22 @@ class ClaudeSDKClient:
67
78
  raise CLIConnectionError("Not connected. Call connect() first.")
68
79
  return self._query
69
80
 
70
- async def connect(self, prompt: str | AsyncIterable[UserPromptMessage] | None = None) -> None:
71
- """Connect to Claude with a prompt or message stream."""
81
+ async def connect(self) -> None:
82
+ """Connect to Claude Code CLI and initialize the session."""
72
83
  from clawd_code_sdk._internal.query import Query
73
84
  from clawd_code_sdk._internal.transport.subprocess_cli import SubprocessCLITransport
74
85
 
75
- # Auto-connect with empty async iterable if no prompt is provided
76
- async def _empty_stream() -> AsyncIterator[UserPromptMessage]:
77
- # Never yields, but indicates that this function is an iterator and
78
- # keeps the connection open.
79
- # This yield is never reached but makes this an async generator
80
- return
81
- yield {} # type: ignore[unreachable]
82
-
83
- actual_prompt = _empty_stream() if prompt is None else prompt
84
86
  # Validate and configure permission settings (matching TypeScript SDK logic)
85
87
  self.options.validate()
86
88
 
87
89
  if self.options.can_use_tool:
88
- # canUseTool callback requires streaming mode (AsyncIterable prompt)
89
- if isinstance(prompt, str):
90
- raise ValueError(
91
- "can_use_tool callback requires streaming mode. "
92
- "Please provide prompt as an AsyncIterable instead of a string."
93
- )
94
-
95
90
  # Automatically set permission_prompt_tool_name to "stdio" for control protocol
96
91
  options = replace(self.options, permission_prompt_tool_name="stdio")
97
92
  else:
98
93
  options = self.options
99
94
 
100
95
  # Use provided custom transport or create subprocess transport
101
- tp = self._custom_transport or SubprocessCLITransport(prompt=actual_prompt, options=options)
96
+ tp = self._custom_transport or SubprocessCLITransport(options=options)
102
97
  self._transport = tp
103
98
  await self._transport.connect()
104
99
  # Extract SDK MCP servers from options
@@ -151,12 +146,6 @@ class ClaudeSDKClient:
151
146
  # Start reading messages and initialize
152
147
  await self._query.start()
153
148
  await self._query.initialize()
154
- # Send the initial prompt
155
- match prompt:
156
- case str() as text:
157
- await self.query(text)
158
- case AsyncIterable() if self._query._tg:
159
- self._query._tg.start_soon(self._query.stream_input, prompt)
160
149
 
161
150
  async def receive_messages(self) -> AsyncIterator[Message]:
162
151
  """Receive all messages from Claude."""
@@ -165,34 +154,50 @@ class ClaudeSDKClient:
165
154
  query = self._ensure_connected()
166
155
  async for data in query.receive_messages():
167
156
  message = parse_message(data)
168
- if isinstance(message, AssistantMessage):
169
- message.raise_if_api_error()
157
+ match message:
158
+ case AssistantMessage():
159
+ message.raise_if_api_error()
160
+ case ResultSuccessMessage() | ResultErrorMessage():
161
+ self.query_usage.accumulate(message.usage)
162
+ self.session_usage.accumulate(message.usage)
170
163
  yield message
171
164
 
172
165
  async def query(
173
166
  self,
174
- prompt: str | AsyncIterable[UserPromptMessage],
167
+ *prompts: str | UserTextPrompt | UserImagePrompt,
175
168
  session_id: str = "default",
169
+ parent_tool_use_id: str | None = None,
176
170
  ) -> None:
177
- """Send a new request in streaming mode."""
171
+ """Send a new user message with one or more content blocks.
172
+
173
+ Args:
174
+ *prompts: One or more content blocks. Strings are converted to
175
+ UserTextPrompt automatically. Pass multiple to combine, e.g.
176
+ ``query(image_prompt, "What's in this image?")``.
177
+ session_id: Session identifier for the message.
178
+ parent_tool_use_id: If responding to a tool use, the tool_use block ID.
179
+ """
180
+ self.query_usage.reset()
178
181
  self._ensure_connected()
179
182
  if not self._transport:
180
183
  raise CLIConnectionError("Not connected. Call connect() first.")
181
- # Handle string prompts
182
- if isinstance(prompt, str):
183
- message = UserPromptMessage(
184
- type="user",
185
- message={"role": "user", "content": prompt},
186
- parent_tool_use_id=None,
187
- session_id=session_id,
188
- )
189
- await self._transport.write(anyenv.dump_json(message) + "\n")
184
+ if not prompts:
185
+ return
186
+ # Collect content blocks
187
+ blocks = [UserTextPrompt(text=p) if isinstance(p, str) else p for p in prompts]
188
+ # Single text block → plain string, otherwise list of content block dicts
189
+ message_content: str | list[dict[str, Any]]
190
+ if len(blocks) == 1 and isinstance(blocks[0], UserTextPrompt):
191
+ message_content = blocks[0].text
190
192
  else:
191
- # Handle AsyncIterable prompts - stream them
192
- async for msg in prompt:
193
- # Ensure session_id is set on each message
194
- msg.setdefault("session_id", session_id)
195
- await self._transport.write(anyenv.dump_json(msg) + "\n")
193
+ message_content = [b.to_content_block() for b in blocks]
194
+ wire_message = {
195
+ "type": "user",
196
+ "message": {"role": "user", "content": message_content},
197
+ "parent_tool_use_id": parent_tool_use_id,
198
+ "session_id": session_id,
199
+ }
200
+ await self._transport.write(anyenv.dump_json(wire_message) + "\n")
196
201
 
197
202
  async def interrupt(self) -> None:
198
203
  """Send interrupt signal (only works with streaming mode)."""
@@ -390,7 +395,7 @@ if __name__ == "__main__":
390
395
  os.environ["ANTHROPIC_API_KEY"] = ""
391
396
 
392
397
  async def main() -> None:
393
- opts = ClaudeAgentOptions(thinking=ThinkingConfigAdaptive(type="adaptive"))
398
+ opts = ClaudeAgentOptions(thinking=ThinkingConfigAdaptive())
394
399
  client = ClaudeSDKClient(opts)
395
400
  await client.connect()
396
401
  await client.query("ultrathink")
@@ -210,6 +210,7 @@ from .content_blocks import (
210
210
  )
211
211
 
212
212
  from .messages import (
213
+ AccumulatedUsage,
213
214
  AssistantMessage,
214
215
  AssistantMessageError,
215
216
  BaseSystemMessage,
@@ -240,8 +241,10 @@ from .messages import (
240
241
  TriggerMetadata,
241
242
  Usage,
242
243
  UserMessage,
243
- UserPromptMessage,
244
- UserPromptMessageContent,
244
+ UserImagePrompt,
245
+ UserTextPrompt,
246
+ UserPrompt,
247
+ ImageMediaType,
245
248
  system_message_adapter,
246
249
  )
247
250
  from .options import ClaudeAgentOptions
@@ -272,14 +275,12 @@ __all__ = [
272
275
  # mcp
273
276
  "JSONRPC_VERSION",
274
277
  "TOOL_INPUT_TYPES",
275
- # output_types (actual tool_use_result wire format)
276
278
  "TOOL_USE_RESULT_TYPES",
279
+ "AccumulatedUsage",
277
280
  "AgentAsyncLaunchedOutput",
278
281
  "AgentCacheCreation",
279
282
  "AgentCompletedOutput",
280
- # agents
281
283
  "AgentDefinition",
282
- # hooks
283
284
  "AgentHookHandler",
284
285
  "AgentHooksConfig",
285
286
  "AgentInput",
@@ -288,14 +289,12 @@ __all__ = [
288
289
  "AgentOutputUsage",
289
290
  "AgentServerToolUse",
290
291
  "AgentSubAgentEnteredOutput",
291
- # base
292
292
  "ApiKeySource",
293
293
  "AskUserQuestion",
294
294
  "AskUserQuestionInput",
295
295
  "AskUserQuestionItem",
296
296
  "AskUserQuestionOption",
297
297
  "AskUserQuestionOutput",
298
- # messages
299
298
  "AssistantMessage",
300
299
  "AssistantMessageError",
301
300
  "AsyncHookJSONOutput",
@@ -308,18 +307,14 @@ __all__ = [
308
307
  "BashOutput",
309
308
  "BashOutputInput",
310
309
  "BashOutputOutput",
311
- # backwards-compat aliases (old tool_use_results.py names)
312
310
  "BashToolUseResult",
313
- # permissions
314
311
  "CanUseTool",
315
- # options
316
312
  "ClaudeAgentOptions",
317
313
  "CommandHookHandler",
318
314
  "CompactBoundarySystemMessage",
319
315
  "ConfigOutput",
320
316
  "ContentBlock",
321
317
  "ContinueLatest",
322
- # control
323
318
  "ControlErrorResponse",
324
319
  "ControlRequestUnion",
325
320
  "ControlResponse",
@@ -352,6 +347,7 @@ __all__ = [
352
347
  "HookResponseSystemMessage",
353
348
  "HookSpecificOutput",
354
349
  "HookStartedSystemMessage",
350
+ "ImageMediaType",
355
351
  "InitSystemMessage",
356
352
  "JSONRPCError",
357
353
  "JSONRPCErrorResponse",
@@ -438,7 +434,6 @@ __all__ = [
438
434
  "SDKHookCallbackRequest",
439
435
  "SDKPermissionDenial",
440
436
  "SDKSessionInfo",
441
- # sandbox
442
437
  "SandboxIgnoreViolations",
443
438
  "SandboxNetworkConfig",
444
439
  "SandboxSettings",
@@ -496,11 +491,12 @@ __all__ = [
496
491
  "UnsubscribeMcpResourceOutput",
497
492
  "UnsubscribePollingOutput",
498
493
  "Usage",
494
+ "UserImagePrompt",
499
495
  "UserMessage",
500
- "UserPromptMessage",
501
- "UserPromptMessageContent",
496
+ "UserPrompt",
502
497
  "UserPromptSubmitHookInput",
503
498
  "UserPromptSubmitHookSpecificOutput",
499
+ "UserTextPrompt",
504
500
  "WebFetchInput",
505
501
  "WebFetchOutput",
506
502
  "WebSearchHit",
@@ -3,7 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import sys
6
- from typing import Literal, TypedDict
6
+ from typing import Literal
7
7
 
8
8
  from pydantic import BaseModel, ConfigDict
9
9
  from pydantic.alias_generators import to_camel
@@ -70,23 +70,23 @@ class ClaudeCodeBaseModel(BaseModel):
70
70
 
71
71
 
72
72
  # Thinking configuration types
73
- class ThinkingConfigAdaptive(TypedDict):
73
+ class ThinkingConfigAdaptive(ClaudeCodeBaseModel):
74
74
  """Adaptive thinking configuration - model decides thinking budget."""
75
75
 
76
- type: Literal["adaptive"]
76
+ type: Literal["adaptive"] = "adaptive"
77
77
 
78
78
 
79
- class ThinkingConfigEnabled(TypedDict):
79
+ class ThinkingConfigEnabled(ClaudeCodeBaseModel):
80
80
  """Enabled thinking configuration with explicit token budget."""
81
81
 
82
- type: Literal["enabled"]
82
+ type: Literal["enabled"] = "enabled"
83
83
  budget_tokens: int
84
84
 
85
85
 
86
- class ThinkingConfigDisabled(TypedDict):
86
+ class ThinkingConfigDisabled(ClaudeCodeBaseModel):
87
87
  """Disabled thinking configuration."""
88
88
 
89
- type: Literal["disabled"]
89
+ type: Literal["disabled"] = "disabled"
90
90
 
91
91
 
92
92
  ThinkingConfig = ThinkingConfigAdaptive | ThinkingConfigEnabled | ThinkingConfigDisabled
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from collections.abc import Sequence # noqa: TC003
6
6
  from dataclasses import dataclass
7
7
  import re
8
- from typing import TYPE_CHECKING, Annotated, Any, Literal, NotRequired, TypedDict
8
+ from typing import TYPE_CHECKING, Annotated, Any, Literal, TypedDict
9
9
 
10
10
  # from anthropic.types import MessageParam
11
11
  from anthropic.types.model import Model # noqa: TC002
@@ -58,29 +58,43 @@ ErrorSubType = Literal[
58
58
  Outcome = Literal["success", "error", "cancelled"]
59
59
 
60
60
 
61
- class UserPromptMessageContent(TypedDict):
62
- """Inner message content for a user prompt."""
61
+ ImageMediaType = Literal["image/png", "image/jpeg", "image/gif", "image/webp"]
63
62
 
64
- role: Literal["user"]
65
- """Message role, always 'user'."""
66
- content: str
67
- """The text content of the message."""
68
63
 
64
+ @dataclass
65
+ class UserTextPrompt:
66
+ """A text-only user prompt."""
69
67
 
70
- class UserPromptMessage(TypedDict):
71
- """A user prompt message sent over the wire to the Claude Code CLI.
68
+ text: str
72
69
 
73
- Used as the element type for streaming prompt iterables.
74
- """
70
+ def to_content_block(self) -> dict[str, Any]:
71
+ """Return the Anthropic API content block dict."""
72
+ return {"type": "text", "text": self.text}
73
+
74
+
75
+ @dataclass
76
+ class UserImagePrompt:
77
+ """A user prompt containing a single base64-encoded image."""
78
+
79
+ image_data: str
80
+ """Base64-encoded image data."""
81
+ media_type: ImageMediaType
82
+ """MIME type of the image."""
75
83
 
76
- type: Literal["user"]
77
- """Message type, always 'user'."""
78
- message: UserPromptMessageContent
79
- """The message content."""
80
- parent_tool_use_id: NotRequired[str | None]
81
- """Optional parent tool use ID for tool result responses."""
82
- session_id: NotRequired[str]
83
- """Session identifier. Auto-injected if not provided."""
84
+ def to_content_block(self) -> dict[str, Any]:
85
+ """Return the Anthropic API content block dict."""
86
+ return {
87
+ "type": "image",
88
+ "source": {
89
+ "type": "base64",
90
+ "media_type": self.media_type,
91
+ "data": self.image_data,
92
+ },
93
+ }
94
+
95
+
96
+ UserPrompt = UserTextPrompt | UserImagePrompt
97
+ """Union type for all user prompt dataclasses."""
84
98
 
85
99
 
86
100
  @dataclass(kw_only=True)
@@ -356,7 +370,7 @@ class HookResponseSystemMessage(BaseSystemMessage):
356
370
 
357
371
 
358
372
  class ModelUsage(TypedDict):
359
- """Token usage from Claude API response."""
373
+ """Cumulative token usage per model, accumulated across the entire session."""
360
374
 
361
375
  inputTokens: int
362
376
  outputTokens: int
@@ -377,7 +391,7 @@ class SDKPermissionDenial(TypedDict):
377
391
 
378
392
 
379
393
  class Usage(TypedDict):
380
- """Token usage from Claude API response."""
394
+ """Token usage from the last API call only (per-turn, not cumulative)."""
381
395
 
382
396
  input_tokens: int
383
397
  output_tokens: int
@@ -385,19 +399,62 @@ class Usage(TypedDict):
385
399
  cache_read_input_tokens: int
386
400
 
387
401
 
402
+ @dataclass
403
+ class AccumulatedUsage:
404
+ """Accumulated token usage, built by summing per-turn Usage values."""
405
+
406
+ input_tokens: int = 0
407
+ output_tokens: int = 0
408
+ cache_creation_input_tokens: int = 0
409
+ cache_read_input_tokens: int = 0
410
+
411
+ @property
412
+ def total_tokens(self) -> int:
413
+ """Sum of all token fields."""
414
+ return (
415
+ self.input_tokens
416
+ + self.output_tokens
417
+ + self.cache_creation_input_tokens
418
+ + self.cache_read_input_tokens
419
+ )
420
+
421
+ def accumulate(self, usage: Usage) -> None:
422
+ """Add a per-turn Usage to this accumulator."""
423
+ self.input_tokens += usage.get("input_tokens", 0)
424
+ self.output_tokens += usage.get("output_tokens", 0)
425
+ self.cache_creation_input_tokens += usage.get("cache_creation_input_tokens", 0)
426
+ self.cache_read_input_tokens += usage.get("cache_read_input_tokens", 0)
427
+
428
+ def reset(self) -> None:
429
+ """Reset all counters to zero."""
430
+ self.input_tokens = 0
431
+ self.output_tokens = 0
432
+ self.cache_creation_input_tokens = 0
433
+ self.cache_read_input_tokens = 0
434
+
435
+
388
436
  @dataclass(kw_only=True)
389
437
  class BaseResultMessage(BaseMessage):
390
- """Base result message with cost and usage information."""
438
+ """Base result message with cost and usage information.
439
+
440
+ Note: Fields use inconsistent scoping. See per-field docs for details.
441
+ """
391
442
 
392
443
  type: Literal["result"] = "result"
393
444
  duration_ms: int
445
+ """Wall-clock time for this query only (per-query)."""
394
446
  duration_api_ms: int
447
+ """Cumulative API time across the entire session."""
395
448
  is_error: bool
396
449
  num_turns: int
450
+ """Number of model turns in this query only (per-query)."""
397
451
  total_cost_usd: float
452
+ """Cumulative cost across the entire session."""
398
453
  usage: Usage
454
+ """Token usage from the last API call only (per-turn)."""
399
455
  stop_reason: StopReason | None
400
456
  modelUsage: dict[str, ModelUsage] # noqa: N815
457
+ """Cumulative token usage per model across the entire session."""
401
458
  permission_denials: list[SDKPermissionDenial]
402
459
 
403
460