claude-agent-sdk 0.0.23__tar.gz → 0.1.1__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.1}/PKG-INFO +13 -4
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/README.md +12 -3
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/pyproject.toml +1 -1
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/src/claude_agent_sdk/_internal/query.py +10 -5
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +56 -14
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/src/claude_agent_sdk/_version.py +1 -1
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/src/claude_agent_sdk/client.py +18 -8
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/src/claude_agent_sdk/types.py +37 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/tests/test_integration.py +1 -1
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/tests/test_subprocess_buffering.py +30 -2
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/tests/test_tool_callbacks.py +4 -4
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/tests/test_transport.py +55 -16
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/tests/test_types.py +2 -2
- 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.1}/.gitignore +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/LICENSE +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/src/claude_agent_sdk/__init__.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/src/claude_agent_sdk/_errors.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/src/claude_agent_sdk/_internal/__init__.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/src/claude_agent_sdk/_internal/client.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/src/claude_agent_sdk/_internal/message_parser.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/src/claude_agent_sdk/_internal/transport/__init__.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/src/claude_agent_sdk/py.typed +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/src/claude_agent_sdk/query.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/tests/conftest.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/tests/test_changelog.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/tests/test_client.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/tests/test_errors.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/tests/test_message_parser.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/tests/test_sdk_mcp_integration.py +0 -0
- {claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/tests/test_streaming_client.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: claude-agent-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1.1
|
|
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
|
|
@@ -43,8 +43,8 @@ pip install claude-agent-sdk
|
|
|
43
43
|
|
|
44
44
|
**Prerequisites:**
|
|
45
45
|
- Python 3.10+
|
|
46
|
-
- Node.js
|
|
47
|
-
- Claude Code
|
|
46
|
+
- Node.js
|
|
47
|
+
- Claude Code 2.0.0+: `npm install -g @anthropic-ai/claude-code`
|
|
48
48
|
|
|
49
49
|
## Quick Start
|
|
50
50
|
|
|
@@ -92,7 +92,7 @@ options = ClaudeAgentOptions(
|
|
|
92
92
|
)
|
|
93
93
|
|
|
94
94
|
async for message in query(
|
|
95
|
-
prompt="Create a hello.py file",
|
|
95
|
+
prompt="Create a hello.py file",
|
|
96
96
|
options=options
|
|
97
97
|
):
|
|
98
98
|
# Process tool use and results
|
|
@@ -304,6 +304,15 @@ See [examples/quick_start.py](examples/quick_start.py) for a complete working ex
|
|
|
304
304
|
|
|
305
305
|
See [examples/streaming_mode.py](examples/streaming_mode.py) for comprehensive examples involving `ClaudeSDKClient`. You can even run interactive examples in IPython from [examples/streaming_mode_ipython.py](examples/streaming_mode_ipython.py).
|
|
306
306
|
|
|
307
|
+
## Migrating from Claude Code SDK
|
|
308
|
+
|
|
309
|
+
If you're upgrading from the Claude Code SDK (versions < 0.1.0), please see the [CHANGELOG.md](CHANGELOG.md#010) for details on breaking changes and new features, including:
|
|
310
|
+
|
|
311
|
+
- `ClaudeCodeOptions` → `ClaudeAgentOptions` rename
|
|
312
|
+
- Merged system prompt configuration
|
|
313
|
+
- Settings isolation and explicit control
|
|
314
|
+
- New programmatic subagents and session forking features
|
|
315
|
+
|
|
307
316
|
## License
|
|
308
317
|
|
|
309
318
|
MIT
|
|
@@ -10,8 +10,8 @@ pip install claude-agent-sdk
|
|
|
10
10
|
|
|
11
11
|
**Prerequisites:**
|
|
12
12
|
- Python 3.10+
|
|
13
|
-
- Node.js
|
|
14
|
-
- Claude Code
|
|
13
|
+
- Node.js
|
|
14
|
+
- Claude Code 2.0.0+: `npm install -g @anthropic-ai/claude-code`
|
|
15
15
|
|
|
16
16
|
## Quick Start
|
|
17
17
|
|
|
@@ -59,7 +59,7 @@ options = ClaudeAgentOptions(
|
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
async for message in query(
|
|
62
|
-
prompt="Create a hello.py file",
|
|
62
|
+
prompt="Create a hello.py file",
|
|
63
63
|
options=options
|
|
64
64
|
):
|
|
65
65
|
# Process tool use and results
|
|
@@ -271,6 +271,15 @@ See [examples/quick_start.py](examples/quick_start.py) for a complete working ex
|
|
|
271
271
|
|
|
272
272
|
See [examples/streaming_mode.py](examples/streaming_mode.py) for comprehensive examples involving `ClaudeSDKClient`. You can even run interactive examples in IPython from [examples/streaming_mode_ipython.py](examples/streaming_mode_ipython.py).
|
|
273
273
|
|
|
274
|
+
## Migrating from Claude Code SDK
|
|
275
|
+
|
|
276
|
+
If you're upgrading from the Claude Code SDK (versions < 0.1.0), please see the [CHANGELOG.md](CHANGELOG.md#010) for details on breaking changes and new features, including:
|
|
277
|
+
|
|
278
|
+
- `ClaudeCodeOptions` → `ClaudeAgentOptions` rename
|
|
279
|
+
- Merged system prompt configuration
|
|
280
|
+
- Settings isolation and explicit control
|
|
281
|
+
- New programmatic subagents and session forking features
|
|
282
|
+
|
|
274
283
|
## License
|
|
275
284
|
|
|
276
285
|
MIT
|
|
@@ -213,13 +213,18 @@ class Query:
|
|
|
213
213
|
|
|
214
214
|
# Convert PermissionResult to expected dict format
|
|
215
215
|
if isinstance(response, PermissionResultAllow):
|
|
216
|
-
response_data = {"
|
|
216
|
+
response_data = {"behavior": "allow"}
|
|
217
217
|
if response.updated_input is not None:
|
|
218
|
-
response_data["
|
|
219
|
-
|
|
218
|
+
response_data["updatedInput"] = response.updated_input
|
|
219
|
+
if response.updated_permissions is not None:
|
|
220
|
+
response_data["updatedPermissions"] = [
|
|
221
|
+
permission.to_dict()
|
|
222
|
+
for permission in response.updated_permissions
|
|
223
|
+
]
|
|
220
224
|
elif isinstance(response, PermissionResultDeny):
|
|
221
|
-
response_data = {"
|
|
222
|
-
|
|
225
|
+
response_data = {"behavior": "deny", "message": response.message}
|
|
226
|
+
if response.interrupt:
|
|
227
|
+
response_data["interrupt"] = response.interrupt
|
|
223
228
|
else:
|
|
224
229
|
raise TypeError(
|
|
225
230
|
f"Tool permission callback must return PermissionResult (PermissionResultAllow or PermissionResultDeny), got {type(response)}"
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
import shutil
|
|
8
|
+
import sys
|
|
7
9
|
from collections.abc import AsyncIterable, AsyncIterator
|
|
8
10
|
from contextlib import suppress
|
|
9
11
|
from dataclasses import asdict
|
|
@@ -24,7 +26,8 @@ from . import Transport
|
|
|
24
26
|
|
|
25
27
|
logger = logging.getLogger(__name__)
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
_DEFAULT_MAX_BUFFER_SIZE = 1024 * 1024 # 1MB buffer limit
|
|
30
|
+
MINIMUM_CLAUDE_CODE_VERSION = "2.0.0"
|
|
28
31
|
|
|
29
32
|
|
|
30
33
|
class SubprocessCLITransport(Transport):
|
|
@@ -48,6 +51,11 @@ class SubprocessCLITransport(Transport):
|
|
|
48
51
|
self._stderr_task_group: anyio.abc.TaskGroup | None = None
|
|
49
52
|
self._ready = False
|
|
50
53
|
self._exit_error: Exception | None = None # Track process exit errors
|
|
54
|
+
self._max_buffer_size = (
|
|
55
|
+
options.max_buffer_size
|
|
56
|
+
if options.max_buffer_size is not None
|
|
57
|
+
else _DEFAULT_MAX_BUFFER_SIZE
|
|
58
|
+
)
|
|
51
59
|
|
|
52
60
|
def _find_cli(self) -> str:
|
|
53
61
|
"""Find Claude Code CLI binary."""
|
|
@@ -66,15 +74,6 @@ class SubprocessCLITransport(Transport):
|
|
|
66
74
|
if path.exists() and path.is_file():
|
|
67
75
|
return str(path)
|
|
68
76
|
|
|
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
77
|
raise CLINotFoundError(
|
|
79
78
|
"Claude Code not found. Install with:\n"
|
|
80
79
|
" npm install -g @anthropic-ai/claude-code\n"
|
|
@@ -206,6 +205,8 @@ class SubprocessCLITransport(Transport):
|
|
|
206
205
|
if self._process:
|
|
207
206
|
return
|
|
208
207
|
|
|
208
|
+
await self._check_claude_version()
|
|
209
|
+
|
|
209
210
|
cmd = self._build_command()
|
|
210
211
|
try:
|
|
211
212
|
# Merge environment variables: system -> user -> SDK required
|
|
@@ -411,12 +412,13 @@ class SubprocessCLITransport(Transport):
|
|
|
411
412
|
# Keep accumulating partial JSON until we can parse it
|
|
412
413
|
json_buffer += json_line
|
|
413
414
|
|
|
414
|
-
if len(json_buffer) >
|
|
415
|
+
if len(json_buffer) > self._max_buffer_size:
|
|
416
|
+
buffer_length = len(json_buffer)
|
|
415
417
|
json_buffer = ""
|
|
416
418
|
raise SDKJSONDecodeError(
|
|
417
|
-
f"JSON message exceeded maximum buffer size of {
|
|
419
|
+
f"JSON message exceeded maximum buffer size of {self._max_buffer_size} bytes",
|
|
418
420
|
ValueError(
|
|
419
|
-
f"Buffer size {
|
|
421
|
+
f"Buffer size {buffer_length} exceeds limit {self._max_buffer_size}"
|
|
420
422
|
),
|
|
421
423
|
)
|
|
422
424
|
|
|
@@ -427,7 +429,7 @@ class SubprocessCLITransport(Transport):
|
|
|
427
429
|
except json.JSONDecodeError:
|
|
428
430
|
# We are speculatively decoding the buffer until we get
|
|
429
431
|
# a full JSON object. If there is an actual issue, we
|
|
430
|
-
# raise an error after
|
|
432
|
+
# raise an error after exceeding the configured limit.
|
|
431
433
|
continue
|
|
432
434
|
|
|
433
435
|
except anyio.ClosedResourceError:
|
|
@@ -451,6 +453,46 @@ class SubprocessCLITransport(Transport):
|
|
|
451
453
|
)
|
|
452
454
|
raise self._exit_error
|
|
453
455
|
|
|
456
|
+
async def _check_claude_version(self) -> None:
|
|
457
|
+
"""Check Claude Code version and warn if below minimum."""
|
|
458
|
+
version_process = None
|
|
459
|
+
try:
|
|
460
|
+
with anyio.fail_after(2): # 2 second timeout
|
|
461
|
+
version_process = await anyio.open_process(
|
|
462
|
+
[self._cli_path, "-v"],
|
|
463
|
+
stdout=PIPE,
|
|
464
|
+
stderr=PIPE,
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
if version_process.stdout:
|
|
468
|
+
stdout_bytes = await version_process.stdout.receive()
|
|
469
|
+
version_output = stdout_bytes.decode().strip()
|
|
470
|
+
|
|
471
|
+
match = re.match(r"([0-9]+\.[0-9]+\.[0-9]+)", version_output)
|
|
472
|
+
if match:
|
|
473
|
+
version = match.group(1)
|
|
474
|
+
version_parts = [int(x) for x in version.split(".")]
|
|
475
|
+
min_parts = [
|
|
476
|
+
int(x) for x in MINIMUM_CLAUDE_CODE_VERSION.split(".")
|
|
477
|
+
]
|
|
478
|
+
|
|
479
|
+
if version_parts < min_parts:
|
|
480
|
+
warning = (
|
|
481
|
+
f"Warning: Claude Code version {version} is unsupported in the Agent SDK. "
|
|
482
|
+
f"Minimum required version is {MINIMUM_CLAUDE_CODE_VERSION}. "
|
|
483
|
+
"Some features may not work correctly."
|
|
484
|
+
)
|
|
485
|
+
logger.warning(warning)
|
|
486
|
+
print(warning, file=sys.stderr)
|
|
487
|
+
except Exception:
|
|
488
|
+
pass
|
|
489
|
+
finally:
|
|
490
|
+
if version_process:
|
|
491
|
+
with suppress(Exception):
|
|
492
|
+
version_process.terminate()
|
|
493
|
+
with suppress(Exception):
|
|
494
|
+
await version_process.wait()
|
|
495
|
+
|
|
454
496
|
def is_ready(self) -> bool:
|
|
455
497
|
"""Check if transport is ready for communication."""
|
|
456
498
|
return self._ready
|
|
@@ -6,6 +6,7 @@ from collections.abc import AsyncIterable, AsyncIterator
|
|
|
6
6
|
from dataclasses import replace
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
+
from . import Transport
|
|
9
10
|
from ._errors import CLIConnectionError
|
|
10
11
|
from .types import ClaudeAgentOptions, HookEvent, HookMatcher, Message, ResultMessage
|
|
11
12
|
|
|
@@ -51,12 +52,17 @@ class ClaudeSDKClient:
|
|
|
51
52
|
exist.
|
|
52
53
|
"""
|
|
53
54
|
|
|
54
|
-
def __init__(
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
options: ClaudeAgentOptions | None = None,
|
|
58
|
+
transport: Transport | None = None,
|
|
59
|
+
):
|
|
55
60
|
"""Initialize Claude SDK client."""
|
|
56
61
|
if options is None:
|
|
57
62
|
options = ClaudeAgentOptions()
|
|
58
63
|
self.options = options
|
|
59
|
-
self.
|
|
64
|
+
self._custom_transport = transport
|
|
65
|
+
self._transport: Transport | None = None
|
|
60
66
|
self._query: Any | None = None
|
|
61
67
|
os.environ["CLAUDE_CODE_ENTRYPOINT"] = "sdk-py-client"
|
|
62
68
|
|
|
@@ -115,10 +121,14 @@ class ClaudeSDKClient:
|
|
|
115
121
|
else:
|
|
116
122
|
options = self.options
|
|
117
123
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
124
|
+
# Use provided custom transport or create subprocess transport
|
|
125
|
+
if self._custom_transport:
|
|
126
|
+
self._transport = self._custom_transport
|
|
127
|
+
else:
|
|
128
|
+
self._transport = SubprocessCLITransport(
|
|
129
|
+
prompt=actual_prompt,
|
|
130
|
+
options=options,
|
|
131
|
+
)
|
|
122
132
|
await self._transport.connect()
|
|
123
133
|
|
|
124
134
|
# Extract SDK MCP servers from options
|
|
@@ -222,7 +232,7 @@ class ClaudeSDKClient:
|
|
|
222
232
|
|
|
223
233
|
Args:
|
|
224
234
|
model: The model to use, or None to use default. Examples:
|
|
225
|
-
- 'claude-sonnet-4-
|
|
235
|
+
- 'claude-sonnet-4-5'
|
|
226
236
|
- 'claude-opus-4-1-20250805'
|
|
227
237
|
- 'claude-opus-4-20250514'
|
|
228
238
|
|
|
@@ -233,7 +243,7 @@ class ClaudeSDKClient:
|
|
|
233
243
|
await client.query("Help me understand this problem")
|
|
234
244
|
|
|
235
245
|
# Switch to a different model for implementation
|
|
236
|
-
await client.set_model('claude-
|
|
246
|
+
await client.set_model('claude-sonnet-4-5')
|
|
237
247
|
await client.query("Now implement the solution")
|
|
238
248
|
```
|
|
239
249
|
"""
|
|
@@ -70,6 +70,42 @@ class PermissionUpdate:
|
|
|
70
70
|
directories: list[str] | None = None
|
|
71
71
|
destination: PermissionUpdateDestination | None = None
|
|
72
72
|
|
|
73
|
+
def to_dict(self) -> dict[str, Any]:
|
|
74
|
+
"""Convert PermissionUpdate to dictionary format matching TypeScript control protocol."""
|
|
75
|
+
result: dict[str, Any] = {
|
|
76
|
+
"type": self.type,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Add destination for all variants
|
|
80
|
+
if self.destination is not None:
|
|
81
|
+
result["destination"] = self.destination
|
|
82
|
+
|
|
83
|
+
# Handle different type variants
|
|
84
|
+
if self.type in ["addRules", "replaceRules", "removeRules"]:
|
|
85
|
+
# Rules-based variants require rules and behavior
|
|
86
|
+
if self.rules is not None:
|
|
87
|
+
result["rules"] = [
|
|
88
|
+
{
|
|
89
|
+
"toolName": rule.tool_name,
|
|
90
|
+
"ruleContent": rule.rule_content,
|
|
91
|
+
}
|
|
92
|
+
for rule in self.rules
|
|
93
|
+
]
|
|
94
|
+
if self.behavior is not None:
|
|
95
|
+
result["behavior"] = self.behavior
|
|
96
|
+
|
|
97
|
+
elif self.type == "setMode":
|
|
98
|
+
# Mode variant requires mode
|
|
99
|
+
if self.mode is not None:
|
|
100
|
+
result["mode"] = self.mode
|
|
101
|
+
|
|
102
|
+
elif self.type in ["addDirectories", "removeDirectories"]:
|
|
103
|
+
# Directory variants require directories
|
|
104
|
+
if self.directories is not None:
|
|
105
|
+
result["directories"] = self.directories
|
|
106
|
+
|
|
107
|
+
return result
|
|
108
|
+
|
|
73
109
|
|
|
74
110
|
# Tool callback types
|
|
75
111
|
@dataclass
|
|
@@ -320,6 +356,7 @@ class ClaudeAgentOptions:
|
|
|
320
356
|
extra_args: dict[str, str | None] = field(
|
|
321
357
|
default_factory=dict
|
|
322
358
|
) # Pass arbitrary CLI flags
|
|
359
|
+
max_buffer_size: int | None = None # Max bytes when buffering CLI stdout
|
|
323
360
|
debug_stderr: Any = (
|
|
324
361
|
sys.stderr
|
|
325
362
|
) # 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
|
|
|
@@ -90,7 +90,7 @@ class TestToolPermissionCallbacks:
|
|
|
90
90
|
# Check response was sent
|
|
91
91
|
assert len(transport.written_messages) == 1
|
|
92
92
|
response = transport.written_messages[0]
|
|
93
|
-
assert '"
|
|
93
|
+
assert '"behavior": "allow"' in response
|
|
94
94
|
|
|
95
95
|
@pytest.mark.asyncio
|
|
96
96
|
async def test_permission_callback_deny(self):
|
|
@@ -125,8 +125,8 @@ class TestToolPermissionCallbacks:
|
|
|
125
125
|
# Check response
|
|
126
126
|
assert len(transport.written_messages) == 1
|
|
127
127
|
response = transport.written_messages[0]
|
|
128
|
-
assert '"
|
|
129
|
-
assert '"
|
|
128
|
+
assert '"behavior": "deny"' in response
|
|
129
|
+
assert '"message": "Security policy violation"' in response
|
|
130
130
|
|
|
131
131
|
@pytest.mark.asyncio
|
|
132
132
|
async def test_permission_callback_input_modification(self):
|
|
@@ -164,7 +164,7 @@ class TestToolPermissionCallbacks:
|
|
|
164
164
|
# Check response includes modified input
|
|
165
165
|
assert len(transport.written_messages) == 1
|
|
166
166
|
response = transport.written_messages[0]
|
|
167
|
-
assert '"
|
|
167
|
+
assert '"behavior": "allow"' in response
|
|
168
168
|
assert '"safe_mode": true' in response
|
|
169
169
|
|
|
170
170
|
@pytest.mark.asyncio
|
|
@@ -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."""
|
|
@@ -106,7 +106,7 @@ class TestSubprocessCLITransport:
|
|
|
106
106
|
options=ClaudeAgentOptions(
|
|
107
107
|
allowed_tools=["Read", "Write"],
|
|
108
108
|
disallowed_tools=["Bash"],
|
|
109
|
-
model="claude-
|
|
109
|
+
model="claude-sonnet-4-5",
|
|
110
110
|
permission_mode="acceptEdits",
|
|
111
111
|
max_turns=5,
|
|
112
112
|
),
|
|
@@ -119,7 +119,7 @@ class TestSubprocessCLITransport:
|
|
|
119
119
|
assert "--disallowedTools" in cmd
|
|
120
120
|
assert "Bash" in cmd
|
|
121
121
|
assert "--model" in cmd
|
|
122
|
-
assert "claude-
|
|
122
|
+
assert "claude-sonnet-4-5" in cmd
|
|
123
123
|
assert "--permission-mode" in cmd
|
|
124
124
|
assert "acceptEdits" in cmd
|
|
125
125
|
assert "--max-turns" in cmd
|
|
@@ -163,6 +163,16 @@ class TestSubprocessCLITransport:
|
|
|
163
163
|
|
|
164
164
|
async def _test():
|
|
165
165
|
with patch("anyio.open_process") as mock_exec:
|
|
166
|
+
# Mock version check process
|
|
167
|
+
mock_version_process = MagicMock()
|
|
168
|
+
mock_version_process.stdout = MagicMock()
|
|
169
|
+
mock_version_process.stdout.receive = AsyncMock(
|
|
170
|
+
return_value=b"2.0.0 (Claude Code)"
|
|
171
|
+
)
|
|
172
|
+
mock_version_process.terminate = MagicMock()
|
|
173
|
+
mock_version_process.wait = AsyncMock()
|
|
174
|
+
|
|
175
|
+
# Mock main process
|
|
166
176
|
mock_process = MagicMock()
|
|
167
177
|
mock_process.returncode = None
|
|
168
178
|
mock_process.terminate = MagicMock()
|
|
@@ -175,7 +185,8 @@ class TestSubprocessCLITransport:
|
|
|
175
185
|
mock_stdin.aclose = AsyncMock()
|
|
176
186
|
mock_process.stdin = mock_stdin
|
|
177
187
|
|
|
178
|
-
|
|
188
|
+
# Return version process first, then main process
|
|
189
|
+
mock_exec.side_effect = [mock_version_process, mock_process]
|
|
179
190
|
|
|
180
191
|
transport = SubprocessCLITransport(
|
|
181
192
|
prompt="test",
|
|
@@ -363,13 +374,25 @@ class TestSubprocessCLITransport:
|
|
|
363
374
|
with patch(
|
|
364
375
|
"anyio.open_process", new_callable=AsyncMock
|
|
365
376
|
) as mock_open_process:
|
|
377
|
+
# Mock version check process
|
|
378
|
+
mock_version_process = MagicMock()
|
|
379
|
+
mock_version_process.stdout = MagicMock()
|
|
380
|
+
mock_version_process.stdout.receive = AsyncMock(
|
|
381
|
+
return_value=b"2.0.0 (Claude Code)"
|
|
382
|
+
)
|
|
383
|
+
mock_version_process.terminate = MagicMock()
|
|
384
|
+
mock_version_process.wait = AsyncMock()
|
|
385
|
+
|
|
386
|
+
# Mock main process
|
|
366
387
|
mock_process = MagicMock()
|
|
367
388
|
mock_process.stdout = MagicMock()
|
|
368
389
|
mock_stdin = MagicMock()
|
|
369
390
|
mock_stdin.aclose = AsyncMock() # Add async aclose method
|
|
370
391
|
mock_process.stdin = mock_stdin
|
|
371
392
|
mock_process.returncode = None
|
|
372
|
-
|
|
393
|
+
|
|
394
|
+
# Return version process first, then main process
|
|
395
|
+
mock_open_process.side_effect = [mock_version_process, mock_process]
|
|
373
396
|
|
|
374
397
|
transport = SubprocessCLITransport(
|
|
375
398
|
prompt="test",
|
|
@@ -379,11 +402,13 @@ class TestSubprocessCLITransport:
|
|
|
379
402
|
|
|
380
403
|
await transport.connect()
|
|
381
404
|
|
|
382
|
-
# Verify open_process was called
|
|
383
|
-
mock_open_process.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
405
|
+
# Verify open_process was called twice (version check + main process)
|
|
406
|
+
assert mock_open_process.call_count == 2
|
|
407
|
+
|
|
408
|
+
# Check the second call (main process) for env vars
|
|
409
|
+
second_call_kwargs = mock_open_process.call_args_list[1].kwargs
|
|
410
|
+
assert "env" in second_call_kwargs
|
|
411
|
+
env_passed = second_call_kwargs["env"]
|
|
387
412
|
|
|
388
413
|
# Check that custom env var was passed
|
|
389
414
|
assert env_passed["MY_TEST_VAR"] == test_value
|
|
@@ -410,13 +435,25 @@ class TestSubprocessCLITransport:
|
|
|
410
435
|
with patch(
|
|
411
436
|
"anyio.open_process", new_callable=AsyncMock
|
|
412
437
|
) as mock_open_process:
|
|
438
|
+
# Mock version check process
|
|
439
|
+
mock_version_process = MagicMock()
|
|
440
|
+
mock_version_process.stdout = MagicMock()
|
|
441
|
+
mock_version_process.stdout.receive = AsyncMock(
|
|
442
|
+
return_value=b"2.0.0 (Claude Code)"
|
|
443
|
+
)
|
|
444
|
+
mock_version_process.terminate = MagicMock()
|
|
445
|
+
mock_version_process.wait = AsyncMock()
|
|
446
|
+
|
|
447
|
+
# Mock main process
|
|
413
448
|
mock_process = MagicMock()
|
|
414
449
|
mock_process.stdout = MagicMock()
|
|
415
450
|
mock_stdin = MagicMock()
|
|
416
451
|
mock_stdin.aclose = AsyncMock() # Add async aclose method
|
|
417
452
|
mock_process.stdin = mock_stdin
|
|
418
453
|
mock_process.returncode = None
|
|
419
|
-
|
|
454
|
+
|
|
455
|
+
# Return version process first, then main process
|
|
456
|
+
mock_open_process.side_effect = [mock_version_process, mock_process]
|
|
420
457
|
|
|
421
458
|
transport = SubprocessCLITransport(
|
|
422
459
|
prompt="test",
|
|
@@ -426,11 +463,13 @@ class TestSubprocessCLITransport:
|
|
|
426
463
|
|
|
427
464
|
await transport.connect()
|
|
428
465
|
|
|
429
|
-
# Verify open_process was called
|
|
430
|
-
mock_open_process.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
466
|
+
# Verify open_process was called twice (version check + main process)
|
|
467
|
+
assert mock_open_process.call_count == 2
|
|
468
|
+
|
|
469
|
+
# Check the second call (main process) for user
|
|
470
|
+
second_call_kwargs = mock_open_process.call_args_list[1].kwargs
|
|
471
|
+
assert "user" in second_call_kwargs
|
|
472
|
+
user_passed = second_call_kwargs["user"]
|
|
434
473
|
|
|
435
474
|
# Check that user was passed
|
|
436
475
|
assert user_passed == "claude"
|
|
@@ -145,7 +145,7 @@ class TestOptions:
|
|
|
145
145
|
def test_claude_code_options_with_model_specification(self):
|
|
146
146
|
"""Test Options with model specification."""
|
|
147
147
|
options = ClaudeAgentOptions(
|
|
148
|
-
model="claude-
|
|
148
|
+
model="claude-sonnet-4-5", permission_prompt_tool_name="CustomTool"
|
|
149
149
|
)
|
|
150
|
-
assert options.model == "claude-
|
|
150
|
+
assert options.model == "claude-sonnet-4-5"
|
|
151
151
|
assert options.permission_prompt_tool_name == "CustomTool"
|
|
@@ -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
|
{claude_agent_sdk-0.0.23 → claude_agent_sdk-0.1.1}/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.1}/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
|