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.
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/.gitignore +1 -1
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/PKG-INFO +2 -1
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/pyproject.toml +2 -1
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/__init__.py +14 -4
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_internal/query.py +1 -35
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_internal/transport/subprocess_cli.py +9 -7
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/client.py +52 -47
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/__init__.py +10 -14
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/base.py +7 -7
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/messages.py +79 -22
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/options.py +10 -21
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/query.py +9 -8
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/session.py +3 -1
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_agents_and_settings.py +2 -2
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_include_partial_messages.py +11 -25
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_slash_commands.py +1 -1
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_stderr_callback.py +2 -2
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_structured_output.py +4 -4
- clawd_code_sdk-0.4.0/tests/mock_claude_server.py +92 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_client.py +6 -6
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_integration.py +1 -1
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_streaming_client.py +10 -50
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_subprocess_buffering.py +8 -8
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_transport.py +56 -66
- clawd_code_sdk-0.3.8/tests/mock_claude_server.py +0 -92
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/LICENSE +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/README.md +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_bundled/.gitignore +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_errors.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_internal/__init__.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_internal/message_parser.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_internal/transport/__init__.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/_version.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/anthropic_types.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/list_sessions.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/mcp_utils.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/agents.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/content_blocks.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/control.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/hooks.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/input_types.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/mcp.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/output_types.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/permissions.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/sandbox.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/models/server_info.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/py.typed +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/storage/ARCHITECTURE.md +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/storage/__init__.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/storage/helpers.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/storage/models.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/storage/replay.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/src/clawd_code_sdk/usage.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/__init__.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/conftest.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/__init__.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_dynamic_control.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_hook_events.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_hooks.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_mcp_tools.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_sdk_mcp_tools.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_subagent_invocation.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/e2e/test_tool_permissions.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/mcp_server.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_changelog.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_errors.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_image.png +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_message_parser.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_sdk_mcp_integration.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_session.py +0 -0
- {clawd_code_sdk-0.3.8 → clawd_code_sdk-0.4.0}/tests/test_tool_callbacks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clawd-code-sdk
|
|
3
|
-
Version: 0.
|
|
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
|
+
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
|
-
"
|
|
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,
|
|
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
|
|
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
|
|
212
|
+
case ThinkingConfigAdaptive():
|
|
211
213
|
cmd.extend(["--max-thinking-tokens", "32000"])
|
|
212
|
-
case
|
|
214
|
+
case ThinkingConfigEnabled(budget_tokens=budget):
|
|
213
215
|
cmd.extend(["--max-thinking-tokens", str(budget)])
|
|
214
|
-
case
|
|
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
|
|
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
|
|
71
|
-
"""Connect to Claude
|
|
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(
|
|
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
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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(
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
"
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
71
|
-
"""A user prompt message sent over the wire to the Claude Code CLI.
|
|
68
|
+
text: str
|
|
72
69
|
|
|
73
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
"""
|
|
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
|
|
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
|
|