claude-agent-sdk 0.0.23__py3-none-any.whl
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.
Potentially problematic release.
This version of claude-agent-sdk might be problematic. Click here for more details.
- claude_agent_sdk/__init__.py +325 -0
- claude_agent_sdk/_errors.py +56 -0
- claude_agent_sdk/_internal/__init__.py +1 -0
- claude_agent_sdk/_internal/client.py +121 -0
- claude_agent_sdk/_internal/message_parser.py +172 -0
- claude_agent_sdk/_internal/query.py +523 -0
- claude_agent_sdk/_internal/transport/__init__.py +68 -0
- claude_agent_sdk/_internal/transport/subprocess_cli.py +456 -0
- claude_agent_sdk/_version.py +3 -0
- claude_agent_sdk/client.py +325 -0
- claude_agent_sdk/py.typed +0 -0
- claude_agent_sdk/query.py +126 -0
- claude_agent_sdk/types.py +412 -0
- claude_agent_sdk-0.0.23.dist-info/METADATA +309 -0
- claude_agent_sdk-0.0.23.dist-info/RECORD +17 -0
- claude_agent_sdk-0.0.23.dist-info/WHEEL +4 -0
- claude_agent_sdk-0.0.23.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
"""Claude SDK for Python."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Generic, TypeVar
|
|
6
|
+
|
|
7
|
+
from ._errors import (
|
|
8
|
+
ClaudeSDKError,
|
|
9
|
+
CLIConnectionError,
|
|
10
|
+
CLIJSONDecodeError,
|
|
11
|
+
CLINotFoundError,
|
|
12
|
+
ProcessError,
|
|
13
|
+
)
|
|
14
|
+
from ._internal.transport import Transport
|
|
15
|
+
from ._version import __version__
|
|
16
|
+
from .client import ClaudeSDKClient
|
|
17
|
+
from .query import query
|
|
18
|
+
from .types import (
|
|
19
|
+
AgentDefinition,
|
|
20
|
+
AssistantMessage,
|
|
21
|
+
CanUseTool,
|
|
22
|
+
ClaudeAgentOptions,
|
|
23
|
+
ContentBlock,
|
|
24
|
+
HookCallback,
|
|
25
|
+
HookContext,
|
|
26
|
+
HookMatcher,
|
|
27
|
+
McpSdkServerConfig,
|
|
28
|
+
McpServerConfig,
|
|
29
|
+
Message,
|
|
30
|
+
PermissionMode,
|
|
31
|
+
PermissionResult,
|
|
32
|
+
PermissionResultAllow,
|
|
33
|
+
PermissionResultDeny,
|
|
34
|
+
PermissionUpdate,
|
|
35
|
+
ResultMessage,
|
|
36
|
+
SettingSource,
|
|
37
|
+
SystemMessage,
|
|
38
|
+
TextBlock,
|
|
39
|
+
ThinkingBlock,
|
|
40
|
+
ToolPermissionContext,
|
|
41
|
+
ToolResultBlock,
|
|
42
|
+
ToolUseBlock,
|
|
43
|
+
UserMessage,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# MCP Server Support
|
|
47
|
+
|
|
48
|
+
T = TypeVar("T")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class SdkMcpTool(Generic[T]):
|
|
53
|
+
"""Definition for an SDK MCP tool."""
|
|
54
|
+
|
|
55
|
+
name: str
|
|
56
|
+
description: str
|
|
57
|
+
input_schema: type[T] | dict[str, Any]
|
|
58
|
+
handler: Callable[[T], Awaitable[dict[str, Any]]]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def tool(
|
|
62
|
+
name: str, description: str, input_schema: type | dict[str, Any]
|
|
63
|
+
) -> Callable[[Callable[[Any], Awaitable[dict[str, Any]]]], SdkMcpTool[Any]]:
|
|
64
|
+
"""Decorator for defining MCP tools with type safety.
|
|
65
|
+
|
|
66
|
+
Creates a tool that can be used with SDK MCP servers. The tool runs
|
|
67
|
+
in-process within your Python application, providing better performance
|
|
68
|
+
than external MCP servers.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
name: Unique identifier for the tool. This is what Claude will use
|
|
72
|
+
to reference the tool in function calls.
|
|
73
|
+
description: Human-readable description of what the tool does.
|
|
74
|
+
This helps Claude understand when to use the tool.
|
|
75
|
+
input_schema: Schema defining the tool's input parameters.
|
|
76
|
+
Can be either:
|
|
77
|
+
- A dictionary mapping parameter names to types (e.g., {"text": str})
|
|
78
|
+
- A TypedDict class for more complex schemas
|
|
79
|
+
- A JSON Schema dictionary for full validation
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
A decorator function that wraps the tool implementation and returns
|
|
83
|
+
an SdkMcpTool instance ready for use with create_sdk_mcp_server().
|
|
84
|
+
|
|
85
|
+
Example:
|
|
86
|
+
Basic tool with simple schema:
|
|
87
|
+
>>> @tool("greet", "Greet a user", {"name": str})
|
|
88
|
+
... async def greet(args):
|
|
89
|
+
... return {"content": [{"type": "text", "text": f"Hello, {args['name']}!"}]}
|
|
90
|
+
|
|
91
|
+
Tool with multiple parameters:
|
|
92
|
+
>>> @tool("add", "Add two numbers", {"a": float, "b": float})
|
|
93
|
+
... async def add_numbers(args):
|
|
94
|
+
... result = args["a"] + args["b"]
|
|
95
|
+
... return {"content": [{"type": "text", "text": f"Result: {result}"}]}
|
|
96
|
+
|
|
97
|
+
Tool with error handling:
|
|
98
|
+
>>> @tool("divide", "Divide two numbers", {"a": float, "b": float})
|
|
99
|
+
... async def divide(args):
|
|
100
|
+
... if args["b"] == 0:
|
|
101
|
+
... return {"content": [{"type": "text", "text": "Error: Division by zero"}], "is_error": True}
|
|
102
|
+
... return {"content": [{"type": "text", "text": f"Result: {args['a'] / args['b']}"}]}
|
|
103
|
+
|
|
104
|
+
Notes:
|
|
105
|
+
- The tool function must be async (defined with async def)
|
|
106
|
+
- The function receives a single dict argument with the input parameters
|
|
107
|
+
- The function should return a dict with a "content" key containing the response
|
|
108
|
+
- Errors can be indicated by including "is_error": True in the response
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def decorator(
|
|
112
|
+
handler: Callable[[Any], Awaitable[dict[str, Any]]],
|
|
113
|
+
) -> SdkMcpTool[Any]:
|
|
114
|
+
return SdkMcpTool(
|
|
115
|
+
name=name,
|
|
116
|
+
description=description,
|
|
117
|
+
input_schema=input_schema,
|
|
118
|
+
handler=handler,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return decorator
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def create_sdk_mcp_server(
|
|
125
|
+
name: str, version: str = "1.0.0", tools: list[SdkMcpTool[Any]] | None = None
|
|
126
|
+
) -> McpSdkServerConfig:
|
|
127
|
+
"""Create an in-process MCP server that runs within your Python application.
|
|
128
|
+
|
|
129
|
+
Unlike external MCP servers that run as separate processes, SDK MCP servers
|
|
130
|
+
run directly in your application's process. This provides:
|
|
131
|
+
- Better performance (no IPC overhead)
|
|
132
|
+
- Simpler deployment (single process)
|
|
133
|
+
- Easier debugging (same process)
|
|
134
|
+
- Direct access to your application's state
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
name: Unique identifier for the server. This name is used to reference
|
|
138
|
+
the server in the mcp_servers configuration.
|
|
139
|
+
version: Server version string. Defaults to "1.0.0". This is for
|
|
140
|
+
informational purposes and doesn't affect functionality.
|
|
141
|
+
tools: List of SdkMcpTool instances created with the @tool decorator.
|
|
142
|
+
These are the functions that Claude can call through this server.
|
|
143
|
+
If None or empty, the server will have no tools (rarely useful).
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
McpSdkServerConfig: A configuration object that can be passed to
|
|
147
|
+
ClaudeAgentOptions.mcp_servers. This config contains the server
|
|
148
|
+
instance and metadata needed for the SDK to route tool calls.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
Simple calculator server:
|
|
152
|
+
>>> @tool("add", "Add numbers", {"a": float, "b": float})
|
|
153
|
+
... async def add(args):
|
|
154
|
+
... return {"content": [{"type": "text", "text": f"Sum: {args['a'] + args['b']}"}]}
|
|
155
|
+
>>>
|
|
156
|
+
>>> @tool("multiply", "Multiply numbers", {"a": float, "b": float})
|
|
157
|
+
... async def multiply(args):
|
|
158
|
+
... return {"content": [{"type": "text", "text": f"Product: {args['a'] * args['b']}"}]}
|
|
159
|
+
>>>
|
|
160
|
+
>>> calculator = create_sdk_mcp_server(
|
|
161
|
+
... name="calculator",
|
|
162
|
+
... version="2.0.0",
|
|
163
|
+
... tools=[add, multiply]
|
|
164
|
+
... )
|
|
165
|
+
>>>
|
|
166
|
+
>>> # Use with Claude
|
|
167
|
+
>>> options = ClaudeAgentOptions(
|
|
168
|
+
... mcp_servers={"calc": calculator},
|
|
169
|
+
... allowed_tools=["add", "multiply"]
|
|
170
|
+
... )
|
|
171
|
+
|
|
172
|
+
Server with application state access:
|
|
173
|
+
>>> class DataStore:
|
|
174
|
+
... def __init__(self):
|
|
175
|
+
... self.items = []
|
|
176
|
+
...
|
|
177
|
+
>>> store = DataStore()
|
|
178
|
+
>>>
|
|
179
|
+
>>> @tool("add_item", "Add item to store", {"item": str})
|
|
180
|
+
... async def add_item(args):
|
|
181
|
+
... store.items.append(args["item"])
|
|
182
|
+
... return {"content": [{"type": "text", "text": f"Added: {args['item']}"}]}
|
|
183
|
+
>>>
|
|
184
|
+
>>> server = create_sdk_mcp_server("store", tools=[add_item])
|
|
185
|
+
|
|
186
|
+
Notes:
|
|
187
|
+
- The server runs in the same process as your Python application
|
|
188
|
+
- Tools have direct access to your application's variables and state
|
|
189
|
+
- No subprocess or IPC overhead for tool calls
|
|
190
|
+
- Server lifecycle is managed automatically by the SDK
|
|
191
|
+
|
|
192
|
+
See Also:
|
|
193
|
+
- tool(): Decorator for creating tool functions
|
|
194
|
+
- ClaudeAgentOptions: Configuration for using servers with query()
|
|
195
|
+
"""
|
|
196
|
+
from mcp.server import Server
|
|
197
|
+
from mcp.types import TextContent, Tool
|
|
198
|
+
|
|
199
|
+
# Create MCP server instance
|
|
200
|
+
server = Server(name, version=version)
|
|
201
|
+
|
|
202
|
+
# Register tools if provided
|
|
203
|
+
if tools:
|
|
204
|
+
# Store tools for access in handlers
|
|
205
|
+
tool_map = {tool_def.name: tool_def for tool_def in tools}
|
|
206
|
+
|
|
207
|
+
# Register list_tools handler to expose available tools
|
|
208
|
+
@server.list_tools() # type: ignore[no-untyped-call,misc]
|
|
209
|
+
async def list_tools() -> list[Tool]:
|
|
210
|
+
"""Return the list of available tools."""
|
|
211
|
+
tool_list = []
|
|
212
|
+
for tool_def in tools:
|
|
213
|
+
# Convert input_schema to JSON Schema format
|
|
214
|
+
if isinstance(tool_def.input_schema, dict):
|
|
215
|
+
# Check if it's already a JSON schema
|
|
216
|
+
if (
|
|
217
|
+
"type" in tool_def.input_schema
|
|
218
|
+
and "properties" in tool_def.input_schema
|
|
219
|
+
):
|
|
220
|
+
schema = tool_def.input_schema
|
|
221
|
+
else:
|
|
222
|
+
# Simple dict mapping names to types - convert to JSON schema
|
|
223
|
+
properties = {}
|
|
224
|
+
for param_name, param_type in tool_def.input_schema.items():
|
|
225
|
+
if param_type is str:
|
|
226
|
+
properties[param_name] = {"type": "string"}
|
|
227
|
+
elif param_type is int:
|
|
228
|
+
properties[param_name] = {"type": "integer"}
|
|
229
|
+
elif param_type is float:
|
|
230
|
+
properties[param_name] = {"type": "number"}
|
|
231
|
+
elif param_type is bool:
|
|
232
|
+
properties[param_name] = {"type": "boolean"}
|
|
233
|
+
else:
|
|
234
|
+
properties[param_name] = {"type": "string"} # Default
|
|
235
|
+
schema = {
|
|
236
|
+
"type": "object",
|
|
237
|
+
"properties": properties,
|
|
238
|
+
"required": list(properties.keys()),
|
|
239
|
+
}
|
|
240
|
+
else:
|
|
241
|
+
# For TypedDict or other types, create basic schema
|
|
242
|
+
schema = {"type": "object", "properties": {}}
|
|
243
|
+
|
|
244
|
+
tool_list.append(
|
|
245
|
+
Tool(
|
|
246
|
+
name=tool_def.name,
|
|
247
|
+
description=tool_def.description,
|
|
248
|
+
inputSchema=schema,
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
return tool_list
|
|
252
|
+
|
|
253
|
+
# Register call_tool handler to execute tools
|
|
254
|
+
@server.call_tool() # type: ignore[misc]
|
|
255
|
+
async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
|
|
256
|
+
"""Execute a tool by name with given arguments."""
|
|
257
|
+
if name not in tool_map:
|
|
258
|
+
raise ValueError(f"Tool '{name}' not found")
|
|
259
|
+
|
|
260
|
+
tool_def = tool_map[name]
|
|
261
|
+
# Call the tool's handler with arguments
|
|
262
|
+
result = await tool_def.handler(arguments)
|
|
263
|
+
|
|
264
|
+
# Convert result to MCP format
|
|
265
|
+
# The decorator expects us to return the content, not a CallToolResult
|
|
266
|
+
# It will wrap our return value in CallToolResult
|
|
267
|
+
content = []
|
|
268
|
+
if "content" in result:
|
|
269
|
+
for item in result["content"]:
|
|
270
|
+
if item.get("type") == "text":
|
|
271
|
+
content.append(TextContent(type="text", text=item["text"]))
|
|
272
|
+
|
|
273
|
+
# Return just the content list - the decorator wraps it
|
|
274
|
+
return content
|
|
275
|
+
|
|
276
|
+
# Return SDK server configuration
|
|
277
|
+
return McpSdkServerConfig(type="sdk", name=name, instance=server)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
__all__ = [
|
|
281
|
+
# Main exports
|
|
282
|
+
"query",
|
|
283
|
+
"__version__",
|
|
284
|
+
# Transport
|
|
285
|
+
"Transport",
|
|
286
|
+
"ClaudeSDKClient",
|
|
287
|
+
# Types
|
|
288
|
+
"PermissionMode",
|
|
289
|
+
"McpServerConfig",
|
|
290
|
+
"McpSdkServerConfig",
|
|
291
|
+
"UserMessage",
|
|
292
|
+
"AssistantMessage",
|
|
293
|
+
"SystemMessage",
|
|
294
|
+
"ResultMessage",
|
|
295
|
+
"Message",
|
|
296
|
+
"ClaudeAgentOptions",
|
|
297
|
+
"TextBlock",
|
|
298
|
+
"ThinkingBlock",
|
|
299
|
+
"ToolUseBlock",
|
|
300
|
+
"ToolResultBlock",
|
|
301
|
+
"ContentBlock",
|
|
302
|
+
# Tool callbacks
|
|
303
|
+
"CanUseTool",
|
|
304
|
+
"ToolPermissionContext",
|
|
305
|
+
"PermissionResult",
|
|
306
|
+
"PermissionResultAllow",
|
|
307
|
+
"PermissionResultDeny",
|
|
308
|
+
"PermissionUpdate",
|
|
309
|
+
"HookCallback",
|
|
310
|
+
"HookContext",
|
|
311
|
+
"HookMatcher",
|
|
312
|
+
# Agent support
|
|
313
|
+
"AgentDefinition",
|
|
314
|
+
"SettingSource",
|
|
315
|
+
# MCP Server Support
|
|
316
|
+
"create_sdk_mcp_server",
|
|
317
|
+
"tool",
|
|
318
|
+
"SdkMcpTool",
|
|
319
|
+
# Errors
|
|
320
|
+
"ClaudeSDKError",
|
|
321
|
+
"CLIConnectionError",
|
|
322
|
+
"CLINotFoundError",
|
|
323
|
+
"ProcessError",
|
|
324
|
+
"CLIJSONDecodeError",
|
|
325
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Error types for Claude SDK."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ClaudeSDKError(Exception):
|
|
7
|
+
"""Base exception for all Claude SDK errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CLIConnectionError(ClaudeSDKError):
|
|
11
|
+
"""Raised when unable to connect to Claude Code."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CLINotFoundError(CLIConnectionError):
|
|
15
|
+
"""Raised when Claude Code is not found or not installed."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self, message: str = "Claude Code not found", cli_path: str | None = None
|
|
19
|
+
):
|
|
20
|
+
if cli_path:
|
|
21
|
+
message = f"{message}: {cli_path}"
|
|
22
|
+
super().__init__(message)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ProcessError(ClaudeSDKError):
|
|
26
|
+
"""Raised when the CLI process fails."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self, message: str, exit_code: int | None = None, stderr: str | None = None
|
|
30
|
+
):
|
|
31
|
+
self.exit_code = exit_code
|
|
32
|
+
self.stderr = stderr
|
|
33
|
+
|
|
34
|
+
if exit_code is not None:
|
|
35
|
+
message = f"{message} (exit code: {exit_code})"
|
|
36
|
+
if stderr:
|
|
37
|
+
message = f"{message}\nError output: {stderr}"
|
|
38
|
+
|
|
39
|
+
super().__init__(message)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CLIJSONDecodeError(ClaudeSDKError):
|
|
43
|
+
"""Raised when unable to decode JSON from CLI output."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, line: str, original_error: Exception):
|
|
46
|
+
self.line = line
|
|
47
|
+
self.original_error = original_error
|
|
48
|
+
super().__init__(f"Failed to decode JSON: {line[:100]}...")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class MessageParseError(ClaudeSDKError):
|
|
52
|
+
"""Raised when unable to parse a message from CLI output."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, message: str, data: dict[str, Any] | None = None):
|
|
55
|
+
self.data = data
|
|
56
|
+
super().__init__(message)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal implementation details."""
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Internal client implementation."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterable, AsyncIterator
|
|
4
|
+
from dataclasses import replace
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from ..types import (
|
|
8
|
+
ClaudeAgentOptions,
|
|
9
|
+
HookEvent,
|
|
10
|
+
HookMatcher,
|
|
11
|
+
Message,
|
|
12
|
+
)
|
|
13
|
+
from .message_parser import parse_message
|
|
14
|
+
from .query import Query
|
|
15
|
+
from .transport import Transport
|
|
16
|
+
from .transport.subprocess_cli import SubprocessCLITransport
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class InternalClient:
|
|
20
|
+
"""Internal client implementation."""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
"""Initialize the internal client."""
|
|
24
|
+
|
|
25
|
+
def _convert_hooks_to_internal_format(
|
|
26
|
+
self, hooks: dict[HookEvent, list[HookMatcher]]
|
|
27
|
+
) -> dict[str, list[dict[str, Any]]]:
|
|
28
|
+
"""Convert HookMatcher format to internal Query format."""
|
|
29
|
+
internal_hooks: dict[str, list[dict[str, Any]]] = {}
|
|
30
|
+
for event, matchers in hooks.items():
|
|
31
|
+
internal_hooks[event] = []
|
|
32
|
+
for matcher in matchers:
|
|
33
|
+
# Convert HookMatcher to internal dict format
|
|
34
|
+
internal_matcher = {
|
|
35
|
+
"matcher": matcher.matcher if hasattr(matcher, "matcher") else None,
|
|
36
|
+
"hooks": matcher.hooks if hasattr(matcher, "hooks") else [],
|
|
37
|
+
}
|
|
38
|
+
internal_hooks[event].append(internal_matcher)
|
|
39
|
+
return internal_hooks
|
|
40
|
+
|
|
41
|
+
async def process_query(
|
|
42
|
+
self,
|
|
43
|
+
prompt: str | AsyncIterable[dict[str, Any]],
|
|
44
|
+
options: ClaudeAgentOptions,
|
|
45
|
+
transport: Transport | None = None,
|
|
46
|
+
) -> AsyncIterator[Message]:
|
|
47
|
+
"""Process a query through transport and Query."""
|
|
48
|
+
|
|
49
|
+
# Validate and configure permission settings (matching TypeScript SDK logic)
|
|
50
|
+
configured_options = options
|
|
51
|
+
if options.can_use_tool:
|
|
52
|
+
# canUseTool callback requires streaming mode (AsyncIterable prompt)
|
|
53
|
+
if isinstance(prompt, str):
|
|
54
|
+
raise ValueError(
|
|
55
|
+
"can_use_tool callback requires streaming mode. "
|
|
56
|
+
"Please provide prompt as an AsyncIterable instead of a string."
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# canUseTool and permission_prompt_tool_name are mutually exclusive
|
|
60
|
+
if options.permission_prompt_tool_name:
|
|
61
|
+
raise ValueError(
|
|
62
|
+
"can_use_tool callback cannot be used with permission_prompt_tool_name. "
|
|
63
|
+
"Please use one or the other."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Automatically set permission_prompt_tool_name to "stdio" for control protocol
|
|
67
|
+
configured_options = replace(options, permission_prompt_tool_name="stdio")
|
|
68
|
+
|
|
69
|
+
# Use provided transport or create subprocess transport
|
|
70
|
+
if transport is not None:
|
|
71
|
+
chosen_transport = transport
|
|
72
|
+
else:
|
|
73
|
+
chosen_transport = SubprocessCLITransport(
|
|
74
|
+
prompt=prompt, options=configured_options
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Connect transport
|
|
78
|
+
await chosen_transport.connect()
|
|
79
|
+
|
|
80
|
+
# Extract SDK MCP servers from configured options
|
|
81
|
+
sdk_mcp_servers = {}
|
|
82
|
+
if configured_options.mcp_servers and isinstance(
|
|
83
|
+
configured_options.mcp_servers, dict
|
|
84
|
+
):
|
|
85
|
+
for name, config in configured_options.mcp_servers.items():
|
|
86
|
+
if isinstance(config, dict) and config.get("type") == "sdk":
|
|
87
|
+
sdk_mcp_servers[name] = config["instance"] # type: ignore[typeddict-item]
|
|
88
|
+
|
|
89
|
+
# Create Query to handle control protocol
|
|
90
|
+
is_streaming = not isinstance(prompt, str)
|
|
91
|
+
query = Query(
|
|
92
|
+
transport=chosen_transport,
|
|
93
|
+
is_streaming_mode=is_streaming,
|
|
94
|
+
can_use_tool=configured_options.can_use_tool,
|
|
95
|
+
hooks=self._convert_hooks_to_internal_format(configured_options.hooks)
|
|
96
|
+
if configured_options.hooks
|
|
97
|
+
else None,
|
|
98
|
+
sdk_mcp_servers=sdk_mcp_servers,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
# Start reading messages
|
|
103
|
+
await query.start()
|
|
104
|
+
|
|
105
|
+
# Initialize if streaming
|
|
106
|
+
if is_streaming:
|
|
107
|
+
await query.initialize()
|
|
108
|
+
|
|
109
|
+
# Stream input if it's an AsyncIterable
|
|
110
|
+
if isinstance(prompt, AsyncIterable) and query._tg:
|
|
111
|
+
# Start streaming in background
|
|
112
|
+
# Create a task that will run in the background
|
|
113
|
+
query._tg.start_soon(query.stream_input, prompt)
|
|
114
|
+
# For string prompts, the prompt is already passed via CLI args
|
|
115
|
+
|
|
116
|
+
# Yield parsed messages
|
|
117
|
+
async for data in query.receive_messages():
|
|
118
|
+
yield parse_message(data)
|
|
119
|
+
|
|
120
|
+
finally:
|
|
121
|
+
await query.close()
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Message parser for Claude Code SDK responses."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .._errors import MessageParseError
|
|
7
|
+
from ..types import (
|
|
8
|
+
AssistantMessage,
|
|
9
|
+
ContentBlock,
|
|
10
|
+
Message,
|
|
11
|
+
ResultMessage,
|
|
12
|
+
StreamEvent,
|
|
13
|
+
SystemMessage,
|
|
14
|
+
TextBlock,
|
|
15
|
+
ThinkingBlock,
|
|
16
|
+
ToolResultBlock,
|
|
17
|
+
ToolUseBlock,
|
|
18
|
+
UserMessage,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_message(data: dict[str, Any]) -> Message:
|
|
25
|
+
"""
|
|
26
|
+
Parse message from CLI output into typed Message objects.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
data: Raw message dictionary from CLI output
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Parsed Message object
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
MessageParseError: If parsing fails or message type is unrecognized
|
|
36
|
+
"""
|
|
37
|
+
if not isinstance(data, dict):
|
|
38
|
+
raise MessageParseError(
|
|
39
|
+
f"Invalid message data type (expected dict, got {type(data).__name__})",
|
|
40
|
+
data,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
message_type = data.get("type")
|
|
44
|
+
if not message_type:
|
|
45
|
+
raise MessageParseError("Message missing 'type' field", data)
|
|
46
|
+
|
|
47
|
+
match message_type:
|
|
48
|
+
case "user":
|
|
49
|
+
try:
|
|
50
|
+
parent_tool_use_id = data.get("parent_tool_use_id")
|
|
51
|
+
if isinstance(data["message"]["content"], list):
|
|
52
|
+
user_content_blocks: list[ContentBlock] = []
|
|
53
|
+
for block in data["message"]["content"]:
|
|
54
|
+
match block["type"]:
|
|
55
|
+
case "text":
|
|
56
|
+
user_content_blocks.append(
|
|
57
|
+
TextBlock(text=block["text"])
|
|
58
|
+
)
|
|
59
|
+
case "tool_use":
|
|
60
|
+
user_content_blocks.append(
|
|
61
|
+
ToolUseBlock(
|
|
62
|
+
id=block["id"],
|
|
63
|
+
name=block["name"],
|
|
64
|
+
input=block["input"],
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
case "tool_result":
|
|
68
|
+
user_content_blocks.append(
|
|
69
|
+
ToolResultBlock(
|
|
70
|
+
tool_use_id=block["tool_use_id"],
|
|
71
|
+
content=block.get("content"),
|
|
72
|
+
is_error=block.get("is_error"),
|
|
73
|
+
)
|
|
74
|
+
)
|
|
75
|
+
return UserMessage(
|
|
76
|
+
content=user_content_blocks,
|
|
77
|
+
parent_tool_use_id=parent_tool_use_id,
|
|
78
|
+
)
|
|
79
|
+
return UserMessage(
|
|
80
|
+
content=data["message"]["content"],
|
|
81
|
+
parent_tool_use_id=parent_tool_use_id,
|
|
82
|
+
)
|
|
83
|
+
except KeyError as e:
|
|
84
|
+
raise MessageParseError(
|
|
85
|
+
f"Missing required field in user message: {e}", data
|
|
86
|
+
) from e
|
|
87
|
+
|
|
88
|
+
case "assistant":
|
|
89
|
+
try:
|
|
90
|
+
content_blocks: list[ContentBlock] = []
|
|
91
|
+
for block in data["message"]["content"]:
|
|
92
|
+
match block["type"]:
|
|
93
|
+
case "text":
|
|
94
|
+
content_blocks.append(TextBlock(text=block["text"]))
|
|
95
|
+
case "thinking":
|
|
96
|
+
content_blocks.append(
|
|
97
|
+
ThinkingBlock(
|
|
98
|
+
thinking=block["thinking"],
|
|
99
|
+
signature=block["signature"],
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
case "tool_use":
|
|
103
|
+
content_blocks.append(
|
|
104
|
+
ToolUseBlock(
|
|
105
|
+
id=block["id"],
|
|
106
|
+
name=block["name"],
|
|
107
|
+
input=block["input"],
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
case "tool_result":
|
|
111
|
+
content_blocks.append(
|
|
112
|
+
ToolResultBlock(
|
|
113
|
+
tool_use_id=block["tool_use_id"],
|
|
114
|
+
content=block.get("content"),
|
|
115
|
+
is_error=block.get("is_error"),
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return AssistantMessage(
|
|
120
|
+
content=content_blocks,
|
|
121
|
+
model=data["message"]["model"],
|
|
122
|
+
parent_tool_use_id=data.get("parent_tool_use_id"),
|
|
123
|
+
)
|
|
124
|
+
except KeyError as e:
|
|
125
|
+
raise MessageParseError(
|
|
126
|
+
f"Missing required field in assistant message: {e}", data
|
|
127
|
+
) from e
|
|
128
|
+
|
|
129
|
+
case "system":
|
|
130
|
+
try:
|
|
131
|
+
return SystemMessage(
|
|
132
|
+
subtype=data["subtype"],
|
|
133
|
+
data=data,
|
|
134
|
+
)
|
|
135
|
+
except KeyError as e:
|
|
136
|
+
raise MessageParseError(
|
|
137
|
+
f"Missing required field in system message: {e}", data
|
|
138
|
+
) from e
|
|
139
|
+
|
|
140
|
+
case "result":
|
|
141
|
+
try:
|
|
142
|
+
return ResultMessage(
|
|
143
|
+
subtype=data["subtype"],
|
|
144
|
+
duration_ms=data["duration_ms"],
|
|
145
|
+
duration_api_ms=data["duration_api_ms"],
|
|
146
|
+
is_error=data["is_error"],
|
|
147
|
+
num_turns=data["num_turns"],
|
|
148
|
+
session_id=data["session_id"],
|
|
149
|
+
total_cost_usd=data.get("total_cost_usd"),
|
|
150
|
+
usage=data.get("usage"),
|
|
151
|
+
result=data.get("result"),
|
|
152
|
+
)
|
|
153
|
+
except KeyError as e:
|
|
154
|
+
raise MessageParseError(
|
|
155
|
+
f"Missing required field in result message: {e}", data
|
|
156
|
+
) from e
|
|
157
|
+
|
|
158
|
+
case "stream_event":
|
|
159
|
+
try:
|
|
160
|
+
return StreamEvent(
|
|
161
|
+
uuid=data["uuid"],
|
|
162
|
+
session_id=data["session_id"],
|
|
163
|
+
event=data["event"],
|
|
164
|
+
parent_tool_use_id=data.get("parent_tool_use_id"),
|
|
165
|
+
)
|
|
166
|
+
except KeyError as e:
|
|
167
|
+
raise MessageParseError(
|
|
168
|
+
f"Missing required field in stream_event message: {e}", data
|
|
169
|
+
) from e
|
|
170
|
+
|
|
171
|
+
case _:
|
|
172
|
+
raise MessageParseError(f"Unknown message type: {message_type}", data)
|