claude-agent-sdk 0.0.23__tar.gz → 0.1.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.
Potentially problematic release.
This version of claude-agent-sdk might be problematic. Click here for more details.
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/PKG-INFO +1 -1
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/pyproject.toml +1 -1
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +11 -14
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_version.py +1 -1
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/types.py +1 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_integration.py +1 -1
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_subprocess_buffering.py +30 -2
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_transport.py +1 -1
- claude_agent_sdk-0.0.23/tests/mock_transport.py +0 -177
- claude_agent_sdk-0.0.23/tests/test_hooks.py +0 -148
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/.gitignore +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/LICENSE +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/README.md +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/__init__.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_errors.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_internal/__init__.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_internal/client.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_internal/message_parser.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_internal/query.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_internal/transport/__init__.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/client.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/py.typed +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/query.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/conftest.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_changelog.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_client.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_errors.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_message_parser.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_sdk_mcp_integration.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_streaming_client.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_tool_callbacks.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/tests/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-agent-sdk
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Python SDK for Claude Code
|
|
5
5
|
Project-URL: Homepage, https://github.com/anthropics/claude-agent-sdk-python
|
|
6
6
|
Project-URL: Documentation, https://docs.anthropic.com/en/docs/claude-code/sdk
|
|
@@ -24,7 +24,7 @@ from . import Transport
|
|
|
24
24
|
|
|
25
25
|
logger = logging.getLogger(__name__)
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
_DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024 # 1MB buffer limit
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
class SubprocessCLITransport(Transport):
|
|
@@ -48,6 +48,11 @@ class SubprocessCLITransport(Transport):
|
|
|
48
48
|
self._stderr_task_group: anyio.abc.TaskGroup | None = None
|
|
49
49
|
self._ready = False
|
|
50
50
|
self._exit_error: Exception | None = None # Track process exit errors
|
|
51
|
+
self._max_buffer_size = (
|
|
52
|
+
options.max_buffer_size
|
|
53
|
+
if options.max_buffer_size is not None
|
|
54
|
+
else _DEFAULT_MAX_BUFFER_SIZE
|
|
55
|
+
)
|
|
51
56
|
|
|
52
57
|
def _find_cli(self) -> str:
|
|
53
58
|
"""Find Claude Code CLI binary."""
|
|
@@ -66,15 +71,6 @@ class SubprocessCLITransport(Transport):
|
|
|
66
71
|
if path.exists() and path.is_file():
|
|
67
72
|
return str(path)
|
|
68
73
|
|
|
69
|
-
node_installed = shutil.which("node") is not None
|
|
70
|
-
|
|
71
|
-
if not node_installed:
|
|
72
|
-
error_msg = "Claude Code requires Node.js, which is not installed.\n\n"
|
|
73
|
-
error_msg += "Install Node.js from: https://nodejs.org/\n"
|
|
74
|
-
error_msg += "\nAfter installing Node.js, install Claude Code:\n"
|
|
75
|
-
error_msg += " npm install -g @anthropic-ai/claude-code"
|
|
76
|
-
raise CLINotFoundError(error_msg)
|
|
77
|
-
|
|
78
74
|
raise CLINotFoundError(
|
|
79
75
|
"Claude Code not found. Install with:\n"
|
|
80
76
|
" npm install -g @anthropic-ai/claude-code\n"
|
|
@@ -411,12 +407,13 @@ class SubprocessCLITransport(Transport):
|
|
|
411
407
|
# Keep accumulating partial JSON until we can parse it
|
|
412
408
|
json_buffer += json_line
|
|
413
409
|
|
|
414
|
-
if len(json_buffer) >
|
|
410
|
+
if len(json_buffer) > self._max_buffer_size:
|
|
411
|
+
buffer_length = len(json_buffer)
|
|
415
412
|
json_buffer = ""
|
|
416
413
|
raise SDKJSONDecodeError(
|
|
417
|
-
f"JSON message exceeded maximum buffer size of {
|
|
414
|
+
f"JSON message exceeded maximum buffer size of {self._max_buffer_size} bytes",
|
|
418
415
|
ValueError(
|
|
419
|
-
f"Buffer size {
|
|
416
|
+
f"Buffer size {buffer_length} exceeds limit {self._max_buffer_size}"
|
|
420
417
|
),
|
|
421
418
|
)
|
|
422
419
|
|
|
@@ -427,7 +424,7 @@ class SubprocessCLITransport(Transport):
|
|
|
427
424
|
except json.JSONDecodeError:
|
|
428
425
|
# We are speculatively decoding the buffer until we get
|
|
429
426
|
# a full JSON object. If there is an actual issue, we
|
|
430
|
-
# raise an error after
|
|
427
|
+
# raise an error after exceeding the configured limit.
|
|
431
428
|
continue
|
|
432
429
|
|
|
433
430
|
except anyio.ClosedResourceError:
|
|
@@ -320,6 +320,7 @@ class ClaudeAgentOptions:
|
|
|
320
320
|
extra_args: dict[str, str | None] = field(
|
|
321
321
|
default_factory=dict
|
|
322
322
|
) # Pass arbitrary CLI flags
|
|
323
|
+
max_buffer_size: int | None = None # Max bytes when buffering CLI stdout
|
|
323
324
|
debug_stderr: Any = (
|
|
324
325
|
sys.stderr
|
|
325
326
|
) # Deprecated: File-like object for debug output. Use stderr callback instead.
|
|
@@ -10,7 +10,7 @@ import pytest
|
|
|
10
10
|
|
|
11
11
|
from claude_agent_sdk._errors import CLIJSONDecodeError
|
|
12
12
|
from claude_agent_sdk._internal.transport.subprocess_cli import (
|
|
13
|
-
|
|
13
|
+
_DEFAULT_MAX_BUFFER_SIZE,
|
|
14
14
|
SubprocessCLITransport,
|
|
15
15
|
)
|
|
16
16
|
from claude_agent_sdk.types import ClaudeAgentOptions
|
|
@@ -237,7 +237,7 @@ class TestSubprocessBuffering:
|
|
|
237
237
|
"""Test that exceeding buffer size raises an appropriate error."""
|
|
238
238
|
|
|
239
239
|
async def _test() -> None:
|
|
240
|
-
huge_incomplete = '{"data": "' + "x" * (
|
|
240
|
+
huge_incomplete = '{"data": "' + "x" * (_DEFAULT_MAX_BUFFER_SIZE + 1000)
|
|
241
241
|
|
|
242
242
|
transport = SubprocessCLITransport(
|
|
243
243
|
prompt="test", options=ClaudeAgentOptions(), cli_path="/usr/bin/claude"
|
|
@@ -260,6 +260,34 @@ class TestSubprocessBuffering:
|
|
|
260
260
|
|
|
261
261
|
anyio.run(_test)
|
|
262
262
|
|
|
263
|
+
def test_buffer_size_option(self) -> None:
|
|
264
|
+
"""Test that the configurable buffer size option is respected."""
|
|
265
|
+
|
|
266
|
+
async def _test() -> None:
|
|
267
|
+
custom_limit = 512
|
|
268
|
+
huge_incomplete = '{"data": "' + "x" * (custom_limit + 10)
|
|
269
|
+
|
|
270
|
+
transport = SubprocessCLITransport(
|
|
271
|
+
prompt="test",
|
|
272
|
+
options=ClaudeAgentOptions(max_buffer_size=custom_limit),
|
|
273
|
+
cli_path="/usr/bin/claude",
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
mock_process = MagicMock()
|
|
277
|
+
mock_process.returncode = None
|
|
278
|
+
mock_process.wait = AsyncMock(return_value=None)
|
|
279
|
+
transport._process = mock_process
|
|
280
|
+
transport._stdout_stream = MockTextReceiveStream([huge_incomplete])
|
|
281
|
+
transport._stderr_stream = MockTextReceiveStream([])
|
|
282
|
+
|
|
283
|
+
with pytest.raises(CLIJSONDecodeError) as exc_info:
|
|
284
|
+
async for _ in transport.read_messages():
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
assert f"maximum buffer size of {custom_limit} bytes" in str(exc_info.value)
|
|
288
|
+
|
|
289
|
+
anyio.run(_test)
|
|
290
|
+
|
|
263
291
|
def test_mixed_complete_and_split_json(self) -> None:
|
|
264
292
|
"""Test handling a mix of complete and split JSON messages."""
|
|
265
293
|
|
|
@@ -25,7 +25,7 @@ class TestSubprocessCLITransport:
|
|
|
25
25
|
):
|
|
26
26
|
SubprocessCLITransport(prompt="test", options=ClaudeAgentOptions())
|
|
27
27
|
|
|
28
|
-
assert "Claude Code
|
|
28
|
+
assert "Claude Code not found" in str(exc_info.value)
|
|
29
29
|
|
|
30
30
|
def test_build_command_basic(self):
|
|
31
31
|
"""Test building basic CLI command."""
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
"""Mock transport implementation for testing."""
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from collections.abc import AsyncIterator
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
import anyio
|
|
8
|
-
|
|
9
|
-
from claude_agent_sdk._internal.transport import Transport
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class MockTransport(Transport):
|
|
13
|
-
"""Mock transport for testing Query and Client behavior.
|
|
14
|
-
|
|
15
|
-
This transport allows tests to:
|
|
16
|
-
- Capture all messages written by the SDK
|
|
17
|
-
- Simulate responses from the CLI
|
|
18
|
-
- Control message flow timing
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
def __init__(self):
|
|
22
|
-
"""Initialize mock transport."""
|
|
23
|
-
self.written_messages: list[dict[str, Any]] = []
|
|
24
|
-
self.messages_to_read: list[dict[str, Any]] = []
|
|
25
|
-
self._ready = False
|
|
26
|
-
self._read_index = 0
|
|
27
|
-
self._write_delay = 0.0 # Optional delay for write operations
|
|
28
|
-
self._read_delay = 0.0 # Optional delay between messages
|
|
29
|
-
|
|
30
|
-
async def connect(self) -> None:
|
|
31
|
-
"""Mark transport as ready."""
|
|
32
|
-
self._ready = True
|
|
33
|
-
|
|
34
|
-
async def write(self, data: str) -> None:
|
|
35
|
-
"""Capture written messages.
|
|
36
|
-
|
|
37
|
-
Args:
|
|
38
|
-
data: JSON string with newline
|
|
39
|
-
"""
|
|
40
|
-
if self._write_delay > 0:
|
|
41
|
-
await anyio.sleep(self._write_delay)
|
|
42
|
-
|
|
43
|
-
# Parse and store the message
|
|
44
|
-
message = json.loads(data.strip())
|
|
45
|
-
self.written_messages.append(message)
|
|
46
|
-
|
|
47
|
-
async def read_messages(self) -> AsyncIterator[dict[str, Any]]:
|
|
48
|
-
"""Yield messages from the configured list.
|
|
49
|
-
|
|
50
|
-
Yields:
|
|
51
|
-
Messages added via add_message_to_read()
|
|
52
|
-
"""
|
|
53
|
-
# Auto-respond to initialize requests
|
|
54
|
-
responded_to_init = False
|
|
55
|
-
|
|
56
|
-
while self._ready:
|
|
57
|
-
# Check for initialize requests we need to respond to
|
|
58
|
-
if not responded_to_init:
|
|
59
|
-
for msg in self.written_messages:
|
|
60
|
-
if (
|
|
61
|
-
msg.get("type") == "control_request"
|
|
62
|
-
and msg.get("request", {}).get("subtype") == "initialize"
|
|
63
|
-
):
|
|
64
|
-
# Auto-send initialize response
|
|
65
|
-
responded_to_init = True
|
|
66
|
-
yield {
|
|
67
|
-
"type": "control_response",
|
|
68
|
-
"response": {
|
|
69
|
-
"subtype": "success",
|
|
70
|
-
"request_id": msg.get("request_id"),
|
|
71
|
-
"response": {
|
|
72
|
-
"commands": [],
|
|
73
|
-
"output_style": "default",
|
|
74
|
-
"hooks": [], # Will be populated by Query.initialize
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
}
|
|
78
|
-
break
|
|
79
|
-
|
|
80
|
-
# Yield any manually added messages
|
|
81
|
-
if self._read_index < len(self.messages_to_read):
|
|
82
|
-
if self._read_delay > 0:
|
|
83
|
-
await anyio.sleep(self._read_delay)
|
|
84
|
-
|
|
85
|
-
message = self.messages_to_read[self._read_index]
|
|
86
|
-
self._read_index += 1
|
|
87
|
-
yield message
|
|
88
|
-
else:
|
|
89
|
-
# Small delay to avoid busy loop
|
|
90
|
-
await anyio.sleep(0.01)
|
|
91
|
-
|
|
92
|
-
async def close(self) -> None:
|
|
93
|
-
"""Mark transport as not ready."""
|
|
94
|
-
self._ready = False
|
|
95
|
-
|
|
96
|
-
def is_ready(self) -> bool:
|
|
97
|
-
"""Check if transport is ready.
|
|
98
|
-
|
|
99
|
-
Returns:
|
|
100
|
-
True if connect() was called and close() wasn't
|
|
101
|
-
"""
|
|
102
|
-
return self._ready
|
|
103
|
-
|
|
104
|
-
async def end_input(self) -> None:
|
|
105
|
-
"""No-op for mock transport."""
|
|
106
|
-
pass
|
|
107
|
-
|
|
108
|
-
# Helper methods for testing
|
|
109
|
-
|
|
110
|
-
def add_message_to_read(self, message: dict[str, Any]) -> None:
|
|
111
|
-
"""Add a message that will be returned by read_messages().
|
|
112
|
-
|
|
113
|
-
Args:
|
|
114
|
-
message: Message dict to be yielded by read_messages()
|
|
115
|
-
"""
|
|
116
|
-
self.messages_to_read.append(message)
|
|
117
|
-
|
|
118
|
-
def add_control_request(self, subtype: str, request_id: str, **kwargs: Any) -> None:
|
|
119
|
-
"""Helper to add a control request message.
|
|
120
|
-
|
|
121
|
-
Args:
|
|
122
|
-
subtype: Control request subtype (e.g., "initialize", "hook_callback")
|
|
123
|
-
request_id: Request ID
|
|
124
|
-
**kwargs: Additional request fields
|
|
125
|
-
"""
|
|
126
|
-
self.add_message_to_read(
|
|
127
|
-
{
|
|
128
|
-
"type": "control_request",
|
|
129
|
-
"request_id": request_id,
|
|
130
|
-
"request": {"subtype": subtype, **kwargs},
|
|
131
|
-
}
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
def get_written_messages(self, msg_type: str | None = None) -> list[dict[str, Any]]:
|
|
135
|
-
"""Get written messages, optionally filtered by type.
|
|
136
|
-
|
|
137
|
-
Args:
|
|
138
|
-
msg_type: Optional message type to filter by
|
|
139
|
-
|
|
140
|
-
Returns:
|
|
141
|
-
List of written messages
|
|
142
|
-
"""
|
|
143
|
-
if msg_type is None:
|
|
144
|
-
return self.written_messages
|
|
145
|
-
return [msg for msg in self.written_messages if msg.get("type") == msg_type]
|
|
146
|
-
|
|
147
|
-
def get_control_responses(self) -> list[dict[str, Any]]:
|
|
148
|
-
"""Get all control response messages that were written.
|
|
149
|
-
|
|
150
|
-
Returns:
|
|
151
|
-
List of control response messages
|
|
152
|
-
"""
|
|
153
|
-
return self.get_written_messages("control_response")
|
|
154
|
-
|
|
155
|
-
def get_last_response(self) -> dict[str, Any] | None:
|
|
156
|
-
"""Get the last written message.
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
Last written message or None if no messages
|
|
160
|
-
"""
|
|
161
|
-
return self.written_messages[-1] if self.written_messages else None
|
|
162
|
-
|
|
163
|
-
def clear(self) -> None:
|
|
164
|
-
"""Clear all messages and reset state."""
|
|
165
|
-
self.written_messages.clear()
|
|
166
|
-
self.messages_to_read.clear()
|
|
167
|
-
self._read_index = 0
|
|
168
|
-
|
|
169
|
-
def set_delays(self, write_delay: float = 0.0, read_delay: float = 0.0) -> None:
|
|
170
|
-
"""Set artificial delays for testing timing issues.
|
|
171
|
-
|
|
172
|
-
Args:
|
|
173
|
-
write_delay: Delay in seconds for write operations
|
|
174
|
-
read_delay: Delay in seconds between read messages
|
|
175
|
-
"""
|
|
176
|
-
self._write_delay = write_delay
|
|
177
|
-
self._read_delay = read_delay
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
"""Tests for the hooks decorator API."""
|
|
2
|
-
|
|
3
|
-
import anyio
|
|
4
|
-
|
|
5
|
-
from claude_agent_sdk.hooks import (
|
|
6
|
-
PreToolUseHookResponse,
|
|
7
|
-
clear_registry,
|
|
8
|
-
get_registry,
|
|
9
|
-
post_tool_use,
|
|
10
|
-
pre_tool_use,
|
|
11
|
-
)
|
|
12
|
-
from claude_agent_sdk.hooks.executor import execute_hook
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class TestHooksAPI:
|
|
16
|
-
"""Test the decorator-based hooks API."""
|
|
17
|
-
|
|
18
|
-
def setup_method(self):
|
|
19
|
-
"""Clear registry before each test."""
|
|
20
|
-
clear_registry()
|
|
21
|
-
|
|
22
|
-
def test_decorator_registers_and_executes(self):
|
|
23
|
-
"""Test that decorators register hooks and they execute properly."""
|
|
24
|
-
|
|
25
|
-
async def _test():
|
|
26
|
-
executed = []
|
|
27
|
-
|
|
28
|
-
# Test pre_tool_use decorator
|
|
29
|
-
@pre_tool_use(matcher="Bash", timeout=5)
|
|
30
|
-
def check_dangerous_commands(tool_name, tool_input):
|
|
31
|
-
executed.append(("pre", tool_name, tool_input.to_dict()))
|
|
32
|
-
if "rm -rf /" in tool_input.get("command", ""):
|
|
33
|
-
return PreToolUseHookResponse(
|
|
34
|
-
permission_decision="deny",
|
|
35
|
-
permission_decision_reason="Dangerous command blocked",
|
|
36
|
-
)
|
|
37
|
-
return None # Allow
|
|
38
|
-
|
|
39
|
-
# Verify registration
|
|
40
|
-
registry = get_registry()
|
|
41
|
-
hooks = registry.get_hooks("PreToolUse")
|
|
42
|
-
assert len(hooks) == 1
|
|
43
|
-
assert hooks[0].matcher == "Bash"
|
|
44
|
-
assert hooks[0].timeout == 5
|
|
45
|
-
|
|
46
|
-
# Test execution with dangerous command
|
|
47
|
-
result = await execute_hook(
|
|
48
|
-
check_dangerous_commands,
|
|
49
|
-
{"tool_name": "Bash", "tool_input": {"command": "rm -rf /"}},
|
|
50
|
-
)
|
|
51
|
-
assert isinstance(result, PreToolUseHookResponse)
|
|
52
|
-
assert result.permission_decision == "deny"
|
|
53
|
-
assert "Dangerous command blocked" in result.permission_decision_reason
|
|
54
|
-
|
|
55
|
-
# Test execution with safe command
|
|
56
|
-
result = await execute_hook(
|
|
57
|
-
check_dangerous_commands,
|
|
58
|
-
{"tool_name": "Bash", "tool_input": {"command": "ls -la"}},
|
|
59
|
-
)
|
|
60
|
-
assert result is None # Allowed
|
|
61
|
-
|
|
62
|
-
# Verify hook was called
|
|
63
|
-
assert len(executed) == 2
|
|
64
|
-
assert executed[0][0] == "pre"
|
|
65
|
-
assert executed[0][1] == "Bash"
|
|
66
|
-
|
|
67
|
-
anyio.run(_test)
|
|
68
|
-
|
|
69
|
-
def test_registry_lifecycle(self):
|
|
70
|
-
"""Test registry clear and isolation between tests."""
|
|
71
|
-
|
|
72
|
-
# Register some hooks
|
|
73
|
-
@pre_tool_use()
|
|
74
|
-
def hook1():
|
|
75
|
-
pass
|
|
76
|
-
|
|
77
|
-
@post_tool_use(matcher="Write")
|
|
78
|
-
def hook2():
|
|
79
|
-
pass
|
|
80
|
-
|
|
81
|
-
registry = get_registry()
|
|
82
|
-
assert len(registry.get_hooks("PreToolUse")) == 1
|
|
83
|
-
assert len(registry.get_hooks("PostToolUse")) == 1
|
|
84
|
-
|
|
85
|
-
# Clear registry
|
|
86
|
-
clear_registry()
|
|
87
|
-
assert len(registry.get_hooks("PreToolUse")) == 0
|
|
88
|
-
assert len(registry.get_hooks("PostToolUse")) == 0
|
|
89
|
-
|
|
90
|
-
# Register new hooks - verify no interference
|
|
91
|
-
@pre_tool_use(matcher="Read")
|
|
92
|
-
def hook3():
|
|
93
|
-
pass
|
|
94
|
-
|
|
95
|
-
assert len(registry.get_hooks("PreToolUse")) == 1
|
|
96
|
-
assert registry.get_hooks("PreToolUse")[0].matcher == "Read"
|
|
97
|
-
|
|
98
|
-
def test_parameter_introspection(self):
|
|
99
|
-
"""Test that hooks only receive the parameters they request."""
|
|
100
|
-
|
|
101
|
-
async def _test():
|
|
102
|
-
received_params = {}
|
|
103
|
-
|
|
104
|
-
# Hook that only wants tool_name
|
|
105
|
-
def minimal_hook(tool_name):
|
|
106
|
-
received_params["minimal"] = {"tool_name": tool_name}
|
|
107
|
-
return None
|
|
108
|
-
|
|
109
|
-
# Hook that wants multiple params
|
|
110
|
-
def full_hook(tool_name, tool_input, tool_use_id):
|
|
111
|
-
received_params["full"] = {
|
|
112
|
-
"tool_name": tool_name,
|
|
113
|
-
"tool_input": tool_input.to_dict(),
|
|
114
|
-
"tool_use_id": tool_use_id,
|
|
115
|
-
}
|
|
116
|
-
return None
|
|
117
|
-
|
|
118
|
-
# Test minimal hook - should only get tool_name
|
|
119
|
-
await execute_hook(
|
|
120
|
-
minimal_hook,
|
|
121
|
-
{
|
|
122
|
-
"tool_name": "TestTool",
|
|
123
|
-
"tool_input": {"data": "test"},
|
|
124
|
-
"extra": "ignored",
|
|
125
|
-
},
|
|
126
|
-
tool_use_id="123",
|
|
127
|
-
)
|
|
128
|
-
|
|
129
|
-
assert "minimal" in received_params
|
|
130
|
-
assert received_params["minimal"] == {"tool_name": "TestTool"}
|
|
131
|
-
|
|
132
|
-
# Test full hook - should get all requested params
|
|
133
|
-
await execute_hook(
|
|
134
|
-
full_hook,
|
|
135
|
-
{
|
|
136
|
-
"tool_name": "TestTool",
|
|
137
|
-
"tool_input": {"data": "test"},
|
|
138
|
-
"extra": "also_ignored",
|
|
139
|
-
},
|
|
140
|
-
tool_use_id="456",
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
assert "full" in received_params
|
|
144
|
-
assert received_params["full"]["tool_name"] == "TestTool"
|
|
145
|
-
assert received_params["full"]["tool_input"] == {"data": "test"}
|
|
146
|
-
assert received_params["full"]["tool_use_id"] == "456"
|
|
147
|
-
|
|
148
|
-
anyio.run(_test)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_internal/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.0}/src/claude_agent_sdk/_internal/message_parser.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|