claude-agent-sdk 0.1.1__tar.gz → 0.1.3__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.1.1 → claude_agent_sdk-0.1.3}/PKG-INFO +1 -1
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/pyproject.toml +1 -1
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/src/claude_agent_sdk/__init__.py +19 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/src/claude_agent_sdk/_internal/query.py +31 -4
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/src/claude_agent_sdk/_version.py +1 -1
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/src/claude_agent_sdk/types.py +181 -19
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/tests/test_streaming_client.py +13 -6
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/tests/test_tool_callbacks.py +201 -2
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/tests/test_transport.py +23 -12
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/.gitignore +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/LICENSE +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/README.md +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/src/claude_agent_sdk/_errors.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/src/claude_agent_sdk/_internal/__init__.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/src/claude_agent_sdk/_internal/client.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/src/claude_agent_sdk/_internal/message_parser.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/src/claude_agent_sdk/_internal/transport/__init__.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/src/claude_agent_sdk/_internal/transport/subprocess_cli.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/src/claude_agent_sdk/client.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/src/claude_agent_sdk/py.typed +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/src/claude_agent_sdk/query.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/tests/conftest.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/tests/test_changelog.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/tests/test_client.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/tests/test_errors.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/tests/test_integration.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/tests/test_message_parser.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/tests/test_sdk_mcp_integration.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/tests/test_subprocess_buffering.py +0 -0
- {claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/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.1.
|
|
3
|
+
Version: 0.1.3
|
|
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
|
|
@@ -18,11 +18,14 @@ from .query import query
|
|
|
18
18
|
from .types import (
|
|
19
19
|
AgentDefinition,
|
|
20
20
|
AssistantMessage,
|
|
21
|
+
BaseHookInput,
|
|
21
22
|
CanUseTool,
|
|
22
23
|
ClaudeAgentOptions,
|
|
23
24
|
ContentBlock,
|
|
24
25
|
HookCallback,
|
|
25
26
|
HookContext,
|
|
27
|
+
HookInput,
|
|
28
|
+
HookJSONOutput,
|
|
26
29
|
HookMatcher,
|
|
27
30
|
McpSdkServerConfig,
|
|
28
31
|
McpServerConfig,
|
|
@@ -32,8 +35,13 @@ from .types import (
|
|
|
32
35
|
PermissionResultAllow,
|
|
33
36
|
PermissionResultDeny,
|
|
34
37
|
PermissionUpdate,
|
|
38
|
+
PostToolUseHookInput,
|
|
39
|
+
PreCompactHookInput,
|
|
40
|
+
PreToolUseHookInput,
|
|
35
41
|
ResultMessage,
|
|
36
42
|
SettingSource,
|
|
43
|
+
StopHookInput,
|
|
44
|
+
SubagentStopHookInput,
|
|
37
45
|
SystemMessage,
|
|
38
46
|
TextBlock,
|
|
39
47
|
ThinkingBlock,
|
|
@@ -41,6 +49,7 @@ from .types import (
|
|
|
41
49
|
ToolResultBlock,
|
|
42
50
|
ToolUseBlock,
|
|
43
51
|
UserMessage,
|
|
52
|
+
UserPromptSubmitHookInput,
|
|
44
53
|
)
|
|
45
54
|
|
|
46
55
|
# MCP Server Support
|
|
@@ -306,8 +315,18 @@ __all__ = [
|
|
|
306
315
|
"PermissionResultAllow",
|
|
307
316
|
"PermissionResultDeny",
|
|
308
317
|
"PermissionUpdate",
|
|
318
|
+
# Hook support
|
|
309
319
|
"HookCallback",
|
|
310
320
|
"HookContext",
|
|
321
|
+
"HookInput",
|
|
322
|
+
"BaseHookInput",
|
|
323
|
+
"PreToolUseHookInput",
|
|
324
|
+
"PostToolUseHookInput",
|
|
325
|
+
"UserPromptSubmitHookInput",
|
|
326
|
+
"StopHookInput",
|
|
327
|
+
"SubagentStopHookInput",
|
|
328
|
+
"PreCompactHookInput",
|
|
329
|
+
"HookJSONOutput",
|
|
311
330
|
"HookMatcher",
|
|
312
331
|
# Agent support
|
|
313
332
|
"AgentDefinition",
|
|
@@ -31,6 +31,25 @@ if TYPE_CHECKING:
|
|
|
31
31
|
logger = logging.getLogger(__name__)
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def _convert_hook_output_for_cli(hook_output: dict[str, Any]) -> dict[str, Any]:
|
|
35
|
+
"""Convert Python-safe field names to CLI-expected field names.
|
|
36
|
+
|
|
37
|
+
The Python SDK uses `async_` and `continue_` to avoid keyword conflicts,
|
|
38
|
+
but the CLI expects `async` and `continue`. This function performs the
|
|
39
|
+
necessary conversion.
|
|
40
|
+
"""
|
|
41
|
+
converted = {}
|
|
42
|
+
for key, value in hook_output.items():
|
|
43
|
+
# Convert Python-safe names to JavaScript names
|
|
44
|
+
if key == "async_":
|
|
45
|
+
converted["async"] = value
|
|
46
|
+
elif key == "continue_":
|
|
47
|
+
converted["continue"] = value
|
|
48
|
+
else:
|
|
49
|
+
converted[key] = value
|
|
50
|
+
return converted
|
|
51
|
+
|
|
52
|
+
|
|
34
53
|
class Query:
|
|
35
54
|
"""Handles bidirectional control protocol on top of Transport.
|
|
36
55
|
|
|
@@ -195,6 +214,7 @@ class Query:
|
|
|
195
214
|
|
|
196
215
|
if subtype == "can_use_tool":
|
|
197
216
|
permission_request: SDKControlPermissionRequest = request_data # type: ignore[assignment]
|
|
217
|
+
original_input = permission_request["input"]
|
|
198
218
|
# Handle tool permission request
|
|
199
219
|
if not self.can_use_tool:
|
|
200
220
|
raise Exception("canUseTool callback is not provided")
|
|
@@ -213,9 +233,14 @@ class Query:
|
|
|
213
233
|
|
|
214
234
|
# Convert PermissionResult to expected dict format
|
|
215
235
|
if isinstance(response, PermissionResultAllow):
|
|
216
|
-
response_data = {
|
|
217
|
-
|
|
218
|
-
|
|
236
|
+
response_data = {
|
|
237
|
+
"behavior": "allow",
|
|
238
|
+
"updatedInput": (
|
|
239
|
+
response.updated_input
|
|
240
|
+
if response.updated_input is not None
|
|
241
|
+
else original_input
|
|
242
|
+
),
|
|
243
|
+
}
|
|
219
244
|
if response.updated_permissions is not None:
|
|
220
245
|
response_data["updatedPermissions"] = [
|
|
221
246
|
permission.to_dict()
|
|
@@ -238,11 +263,13 @@ class Query:
|
|
|
238
263
|
if not callback:
|
|
239
264
|
raise Exception(f"No hook callback found for ID: {callback_id}")
|
|
240
265
|
|
|
241
|
-
|
|
266
|
+
hook_output = await callback(
|
|
242
267
|
request_data.get("input"),
|
|
243
268
|
request_data.get("tool_use_id"),
|
|
244
269
|
{"signal": None}, # TODO: Add abort signal support
|
|
245
270
|
)
|
|
271
|
+
# Convert Python-safe field names (async_, continue_) to CLI-expected names (async, continue)
|
|
272
|
+
response_data = _convert_hook_output_for_cli(hook_output)
|
|
246
273
|
|
|
247
274
|
elif subtype == "mcp_message":
|
|
248
275
|
# Handle SDK MCP request
|
|
@@ -157,35 +157,197 @@ HookEvent = (
|
|
|
157
157
|
)
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
# Hook input types - strongly typed for each hook event
|
|
161
|
+
class BaseHookInput(TypedDict):
|
|
162
|
+
"""Base hook input fields present across many hook events."""
|
|
163
|
+
|
|
164
|
+
session_id: str
|
|
165
|
+
transcript_path: str
|
|
166
|
+
cwd: str
|
|
167
|
+
permission_mode: NotRequired[str]
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class PreToolUseHookInput(BaseHookInput):
|
|
171
|
+
"""Input data for PreToolUse hook events."""
|
|
172
|
+
|
|
173
|
+
hook_event_name: Literal["PreToolUse"]
|
|
174
|
+
tool_name: str
|
|
175
|
+
tool_input: dict[str, Any]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class PostToolUseHookInput(BaseHookInput):
|
|
179
|
+
"""Input data for PostToolUse hook events."""
|
|
180
|
+
|
|
181
|
+
hook_event_name: Literal["PostToolUse"]
|
|
182
|
+
tool_name: str
|
|
183
|
+
tool_input: dict[str, Any]
|
|
184
|
+
tool_response: Any
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
class UserPromptSubmitHookInput(BaseHookInput):
|
|
188
|
+
"""Input data for UserPromptSubmit hook events."""
|
|
189
|
+
|
|
190
|
+
hook_event_name: Literal["UserPromptSubmit"]
|
|
191
|
+
prompt: str
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class StopHookInput(BaseHookInput):
|
|
195
|
+
"""Input data for Stop hook events."""
|
|
196
|
+
|
|
197
|
+
hook_event_name: Literal["Stop"]
|
|
198
|
+
stop_hook_active: bool
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class SubagentStopHookInput(BaseHookInput):
|
|
202
|
+
"""Input data for SubagentStop hook events."""
|
|
203
|
+
|
|
204
|
+
hook_event_name: Literal["SubagentStop"]
|
|
205
|
+
stop_hook_active: bool
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class PreCompactHookInput(BaseHookInput):
|
|
209
|
+
"""Input data for PreCompact hook events."""
|
|
210
|
+
|
|
211
|
+
hook_event_name: Literal["PreCompact"]
|
|
212
|
+
trigger: Literal["manual", "auto"]
|
|
213
|
+
custom_instructions: str | None
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# Union type for all hook inputs
|
|
217
|
+
HookInput = (
|
|
218
|
+
PreToolUseHookInput
|
|
219
|
+
| PostToolUseHookInput
|
|
220
|
+
| UserPromptSubmitHookInput
|
|
221
|
+
| StopHookInput
|
|
222
|
+
| SubagentStopHookInput
|
|
223
|
+
| PreCompactHookInput
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
# Hook-specific output types
|
|
228
|
+
class PreToolUseHookSpecificOutput(TypedDict):
|
|
229
|
+
"""Hook-specific output for PreToolUse events."""
|
|
230
|
+
|
|
231
|
+
hookEventName: Literal["PreToolUse"]
|
|
232
|
+
permissionDecision: NotRequired[Literal["allow", "deny", "ask"]]
|
|
233
|
+
permissionDecisionReason: NotRequired[str]
|
|
234
|
+
updatedInput: NotRequired[dict[str, Any]]
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
class PostToolUseHookSpecificOutput(TypedDict):
|
|
238
|
+
"""Hook-specific output for PostToolUse events."""
|
|
239
|
+
|
|
240
|
+
hookEventName: Literal["PostToolUse"]
|
|
241
|
+
additionalContext: NotRequired[str]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class UserPromptSubmitHookSpecificOutput(TypedDict):
|
|
245
|
+
"""Hook-specific output for UserPromptSubmit events."""
|
|
246
|
+
|
|
247
|
+
hookEventName: Literal["UserPromptSubmit"]
|
|
248
|
+
additionalContext: NotRequired[str]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class SessionStartHookSpecificOutput(TypedDict):
|
|
252
|
+
"""Hook-specific output for SessionStart events."""
|
|
253
|
+
|
|
254
|
+
hookEventName: Literal["SessionStart"]
|
|
255
|
+
additionalContext: NotRequired[str]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
HookSpecificOutput = (
|
|
259
|
+
PreToolUseHookSpecificOutput
|
|
260
|
+
| PostToolUseHookSpecificOutput
|
|
261
|
+
| UserPromptSubmitHookSpecificOutput
|
|
262
|
+
| SessionStartHookSpecificOutput
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
160
266
|
# See https://docs.anthropic.com/en/docs/claude-code/hooks#advanced%3A-json-output
|
|
161
|
-
# for documentation of the output types.
|
|
162
|
-
#
|
|
163
|
-
|
|
164
|
-
|
|
267
|
+
# for documentation of the output types.
|
|
268
|
+
#
|
|
269
|
+
# IMPORTANT: The Python SDK uses `async_` and `continue_` (with underscores) to avoid
|
|
270
|
+
# Python keyword conflicts. These fields are automatically converted to `async` and
|
|
271
|
+
# `continue` when sent to the CLI. You should use the underscore versions in your
|
|
272
|
+
# Python code.
|
|
273
|
+
class AsyncHookJSONOutput(TypedDict):
|
|
274
|
+
"""Async hook output that defers hook execution.
|
|
275
|
+
|
|
276
|
+
Fields:
|
|
277
|
+
async_: Set to True to defer hook execution. Note: This is converted to
|
|
278
|
+
"async" when sent to the CLI - use "async_" in your Python code.
|
|
279
|
+
asyncTimeout: Optional timeout in milliseconds for the async operation.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
async_: Literal[
|
|
283
|
+
True
|
|
284
|
+
] # Using async_ to avoid Python keyword (converted to "async" for CLI)
|
|
285
|
+
asyncTimeout: NotRequired[int]
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class SyncHookJSONOutput(TypedDict):
|
|
289
|
+
"""Synchronous hook output with control and decision fields.
|
|
290
|
+
|
|
291
|
+
This defines the structure for hook callbacks to control execution and provide
|
|
292
|
+
feedback to Claude.
|
|
293
|
+
|
|
294
|
+
Common Control Fields:
|
|
295
|
+
continue_: Whether Claude should proceed after hook execution (default: True).
|
|
296
|
+
Note: This is converted to "continue" when sent to the CLI.
|
|
297
|
+
suppressOutput: Hide stdout from transcript mode (default: False).
|
|
298
|
+
stopReason: Message shown when continue is False.
|
|
299
|
+
|
|
300
|
+
Decision Fields:
|
|
301
|
+
decision: Set to "block" to indicate blocking behavior.
|
|
302
|
+
systemMessage: Warning message displayed to the user.
|
|
303
|
+
reason: Feedback message for Claude about the decision.
|
|
304
|
+
|
|
305
|
+
Hook-Specific Output:
|
|
306
|
+
hookSpecificOutput: Event-specific controls (e.g., permissionDecision for
|
|
307
|
+
PreToolUse, additionalContext for PostToolUse).
|
|
308
|
+
|
|
309
|
+
Note: The CLI documentation shows field names without underscores ("async", "continue"),
|
|
310
|
+
but Python code should use the underscore versions ("async_", "continue_") as they
|
|
311
|
+
are automatically converted.
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
# Common control fields
|
|
315
|
+
continue_: NotRequired[
|
|
316
|
+
bool
|
|
317
|
+
] # Using continue_ to avoid Python keyword (converted to "continue" for CLI)
|
|
318
|
+
suppressOutput: NotRequired[bool]
|
|
319
|
+
stopReason: NotRequired[str]
|
|
320
|
+
|
|
321
|
+
# Decision fields
|
|
322
|
+
# Note: "approve" is deprecated for PreToolUse (use permissionDecision instead)
|
|
323
|
+
# For other hooks, only "block" is meaningful
|
|
165
324
|
decision: NotRequired[Literal["block"]]
|
|
166
|
-
# Optionally add a system message that is not visible to Claude but saved in
|
|
167
|
-
# the chat transcript.
|
|
168
325
|
systemMessage: NotRequired[str]
|
|
169
|
-
|
|
170
|
-
# for guidance.
|
|
171
|
-
hookSpecificOutput: NotRequired[Any]
|
|
326
|
+
reason: NotRequired[str]
|
|
172
327
|
|
|
328
|
+
# Hook-specific outputs
|
|
329
|
+
hookSpecificOutput: NotRequired[HookSpecificOutput]
|
|
173
330
|
|
|
174
|
-
@dataclass
|
|
175
|
-
class HookContext:
|
|
176
|
-
"""Context information for hook callbacks."""
|
|
177
331
|
|
|
178
|
-
|
|
332
|
+
HookJSONOutput = AsyncHookJSONOutput | SyncHookJSONOutput
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
class HookContext(TypedDict):
|
|
336
|
+
"""Context information for hook callbacks.
|
|
337
|
+
|
|
338
|
+
Fields:
|
|
339
|
+
signal: Reserved for future abort signal support. Currently always None.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
signal: Any | None # Future: abort signal support
|
|
179
343
|
|
|
180
344
|
|
|
181
345
|
HookCallback = Callable[
|
|
182
346
|
# HookCallback input parameters:
|
|
183
|
-
# - input
|
|
184
|
-
#
|
|
185
|
-
#
|
|
186
|
-
|
|
187
|
-
# - context
|
|
188
|
-
[dict[str, Any], str | None, HookContext],
|
|
347
|
+
# - input: Strongly-typed hook input with discriminated unions based on hook_event_name
|
|
348
|
+
# - tool_use_id: Optional tool use identifier
|
|
349
|
+
# - context: Hook context with abort signal support (currently placeholder)
|
|
350
|
+
[HookInput, str | None, HookContext],
|
|
189
351
|
Awaitable[HookJSONOutput],
|
|
190
352
|
]
|
|
191
353
|
|
|
@@ -633,21 +633,28 @@ assert '"Second"' in stdin_messages[1]
|
|
|
633
633
|
print('{"type": "result", "subtype": "success", "duration_ms": 100, "duration_api_ms": 50, "is_error": false, "num_turns": 1, "session_id": "test", "total_cost_usd": 0.001}')
|
|
634
634
|
""")
|
|
635
635
|
|
|
636
|
-
|
|
636
|
+
# Make script executable (Unix-style systems)
|
|
637
|
+
if sys.platform != "win32":
|
|
638
|
+
Path(test_script).chmod(0o755)
|
|
637
639
|
|
|
638
640
|
try:
|
|
639
|
-
# Mock _find_cli to return
|
|
641
|
+
# Mock _find_cli to return the test script path directly
|
|
640
642
|
with patch.object(
|
|
641
|
-
SubprocessCLITransport, "_find_cli", return_value=
|
|
643
|
+
SubprocessCLITransport, "_find_cli", return_value=test_script
|
|
642
644
|
):
|
|
643
|
-
# Mock _build_command to
|
|
645
|
+
# Mock _build_command to properly execute Python script
|
|
644
646
|
original_build_command = SubprocessCLITransport._build_command
|
|
645
647
|
|
|
646
648
|
def mock_build_command(self):
|
|
647
649
|
# Get original command
|
|
648
650
|
cmd = original_build_command(self)
|
|
649
|
-
#
|
|
650
|
-
|
|
651
|
+
# On Windows, we need to use python interpreter to run the script
|
|
652
|
+
if sys.platform == "win32":
|
|
653
|
+
# Replace first element with python interpreter and script
|
|
654
|
+
cmd[0:1] = [sys.executable, test_script]
|
|
655
|
+
else:
|
|
656
|
+
# On Unix, just use the script directly
|
|
657
|
+
cmd[0] = test_script
|
|
651
658
|
return cmd
|
|
652
659
|
|
|
653
660
|
with patch.object(
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
"""Tests for tool permission callbacks and hook callbacks."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
|
|
3
5
|
import pytest
|
|
4
6
|
|
|
5
7
|
from claude_agent_sdk import (
|
|
6
8
|
ClaudeAgentOptions,
|
|
7
9
|
HookContext,
|
|
10
|
+
HookInput,
|
|
11
|
+
HookJSONOutput,
|
|
8
12
|
HookMatcher,
|
|
9
13
|
PermissionResultAllow,
|
|
10
14
|
PermissionResultDeny,
|
|
@@ -213,7 +217,7 @@ class TestHookCallbacks:
|
|
|
213
217
|
hook_calls = []
|
|
214
218
|
|
|
215
219
|
async def test_hook(
|
|
216
|
-
input_data:
|
|
220
|
+
input_data: HookInput, tool_use_id: str | None, context: HookContext
|
|
217
221
|
) -> dict:
|
|
218
222
|
hook_calls.append({"input": input_data, "tool_use_id": tool_use_id})
|
|
219
223
|
return {"processed": True}
|
|
@@ -257,6 +261,201 @@ class TestHookCallbacks:
|
|
|
257
261
|
last_response = transport.written_messages[-1]
|
|
258
262
|
assert '"processed": true' in last_response
|
|
259
263
|
|
|
264
|
+
@pytest.mark.asyncio
|
|
265
|
+
async def test_hook_output_fields(self):
|
|
266
|
+
"""Test that all SyncHookJSONOutput fields are properly handled."""
|
|
267
|
+
|
|
268
|
+
# Test all SyncHookJSONOutput fields together
|
|
269
|
+
async def comprehensive_hook(
|
|
270
|
+
input_data: HookInput, tool_use_id: str | None, context: HookContext
|
|
271
|
+
) -> HookJSONOutput:
|
|
272
|
+
return {
|
|
273
|
+
# Control fields
|
|
274
|
+
"continue_": True,
|
|
275
|
+
"suppressOutput": False,
|
|
276
|
+
"stopReason": "Test stop reason",
|
|
277
|
+
# Decision fields
|
|
278
|
+
"decision": "block",
|
|
279
|
+
"systemMessage": "Test system message",
|
|
280
|
+
"reason": "Test reason for blocking",
|
|
281
|
+
# Hook-specific output with all PreToolUse fields
|
|
282
|
+
"hookSpecificOutput": {
|
|
283
|
+
"hookEventName": "PreToolUse",
|
|
284
|
+
"permissionDecision": "deny",
|
|
285
|
+
"permissionDecisionReason": "Security policy violation",
|
|
286
|
+
"updatedInput": {"modified": "input"},
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
transport = MockTransport()
|
|
291
|
+
hooks = {
|
|
292
|
+
"PreToolUse": [
|
|
293
|
+
{"matcher": {"tool": "TestTool"}, "hooks": [comprehensive_hook]}
|
|
294
|
+
]
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
query = Query(
|
|
298
|
+
transport=transport, is_streaming_mode=True, can_use_tool=None, hooks=hooks
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
callback_id = "test_comprehensive_hook"
|
|
302
|
+
query.hook_callbacks[callback_id] = comprehensive_hook
|
|
303
|
+
|
|
304
|
+
request = {
|
|
305
|
+
"type": "control_request",
|
|
306
|
+
"request_id": "test-comprehensive",
|
|
307
|
+
"request": {
|
|
308
|
+
"subtype": "hook_callback",
|
|
309
|
+
"callback_id": callback_id,
|
|
310
|
+
"input": {"test": "data"},
|
|
311
|
+
"tool_use_id": "tool-456",
|
|
312
|
+
},
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
await query._handle_control_request(request)
|
|
316
|
+
|
|
317
|
+
# Check response contains all the fields
|
|
318
|
+
assert len(transport.written_messages) > 0
|
|
319
|
+
last_response = transport.written_messages[-1]
|
|
320
|
+
|
|
321
|
+
# Parse the JSON response
|
|
322
|
+
response_data = json.loads(last_response)
|
|
323
|
+
# The hook result is nested at response.response
|
|
324
|
+
result = response_data["response"]["response"]
|
|
325
|
+
|
|
326
|
+
# Verify control fields are present and converted to CLI format
|
|
327
|
+
assert result.get("continue") is True, (
|
|
328
|
+
"continue_ should be converted to continue"
|
|
329
|
+
)
|
|
330
|
+
assert "continue_" not in result, "continue_ should not appear in CLI output"
|
|
331
|
+
assert result.get("suppressOutput") is False
|
|
332
|
+
assert result.get("stopReason") == "Test stop reason"
|
|
333
|
+
|
|
334
|
+
# Verify decision fields are present
|
|
335
|
+
assert result.get("decision") == "block"
|
|
336
|
+
assert result.get("reason") == "Test reason for blocking"
|
|
337
|
+
assert result.get("systemMessage") == "Test system message"
|
|
338
|
+
|
|
339
|
+
# Verify hook-specific output is present
|
|
340
|
+
hook_output = result.get("hookSpecificOutput", {})
|
|
341
|
+
assert hook_output.get("hookEventName") == "PreToolUse"
|
|
342
|
+
assert hook_output.get("permissionDecision") == "deny"
|
|
343
|
+
assert (
|
|
344
|
+
hook_output.get("permissionDecisionReason") == "Security policy violation"
|
|
345
|
+
)
|
|
346
|
+
assert "updatedInput" in hook_output
|
|
347
|
+
|
|
348
|
+
@pytest.mark.asyncio
|
|
349
|
+
async def test_async_hook_output(self):
|
|
350
|
+
"""Test AsyncHookJSONOutput type with proper async fields."""
|
|
351
|
+
|
|
352
|
+
async def async_hook(
|
|
353
|
+
input_data: HookInput, tool_use_id: str | None, context: HookContext
|
|
354
|
+
) -> HookJSONOutput:
|
|
355
|
+
# Test that async hooks properly use async_ and asyncTimeout fields
|
|
356
|
+
return {
|
|
357
|
+
"async_": True,
|
|
358
|
+
"asyncTimeout": 5000,
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
transport = MockTransport()
|
|
362
|
+
hooks = {"PreToolUse": [{"matcher": None, "hooks": [async_hook]}]}
|
|
363
|
+
|
|
364
|
+
query = Query(
|
|
365
|
+
transport=transport, is_streaming_mode=True, can_use_tool=None, hooks=hooks
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
callback_id = "test_async_hook"
|
|
369
|
+
query.hook_callbacks[callback_id] = async_hook
|
|
370
|
+
|
|
371
|
+
request = {
|
|
372
|
+
"type": "control_request",
|
|
373
|
+
"request_id": "test-async",
|
|
374
|
+
"request": {
|
|
375
|
+
"subtype": "hook_callback",
|
|
376
|
+
"callback_id": callback_id,
|
|
377
|
+
"input": {"test": "async_data"},
|
|
378
|
+
"tool_use_id": None,
|
|
379
|
+
},
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
await query._handle_control_request(request)
|
|
383
|
+
|
|
384
|
+
# Check response contains async fields
|
|
385
|
+
assert len(transport.written_messages) > 0
|
|
386
|
+
last_response = transport.written_messages[-1]
|
|
387
|
+
|
|
388
|
+
# Parse the JSON response
|
|
389
|
+
response_data = json.loads(last_response)
|
|
390
|
+
# The hook result is nested at response.response
|
|
391
|
+
result = response_data["response"]["response"]
|
|
392
|
+
|
|
393
|
+
# The SDK should convert async_ to "async" for CLI compatibility
|
|
394
|
+
assert result.get("async") is True, "async_ should be converted to async"
|
|
395
|
+
assert "async_" not in result, "async_ should not appear in CLI output"
|
|
396
|
+
assert result.get("asyncTimeout") == 5000
|
|
397
|
+
|
|
398
|
+
@pytest.mark.asyncio
|
|
399
|
+
async def test_field_name_conversion(self):
|
|
400
|
+
"""Test that Python-safe field names (async_, continue_) are converted to CLI format (async, continue)."""
|
|
401
|
+
|
|
402
|
+
async def conversion_test_hook(
|
|
403
|
+
input_data: HookInput, tool_use_id: str | None, context: HookContext
|
|
404
|
+
) -> HookJSONOutput:
|
|
405
|
+
# Return both async_ and continue_ to test conversion
|
|
406
|
+
return {
|
|
407
|
+
"async_": True,
|
|
408
|
+
"asyncTimeout": 10000,
|
|
409
|
+
"continue_": False,
|
|
410
|
+
"stopReason": "Testing field conversion",
|
|
411
|
+
"systemMessage": "Fields should be converted",
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
transport = MockTransport()
|
|
415
|
+
hooks = {"PreToolUse": [{"matcher": None, "hooks": [conversion_test_hook]}]}
|
|
416
|
+
|
|
417
|
+
query = Query(
|
|
418
|
+
transport=transport, is_streaming_mode=True, can_use_tool=None, hooks=hooks
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
callback_id = "test_conversion"
|
|
422
|
+
query.hook_callbacks[callback_id] = conversion_test_hook
|
|
423
|
+
|
|
424
|
+
request = {
|
|
425
|
+
"type": "control_request",
|
|
426
|
+
"request_id": "test-conversion",
|
|
427
|
+
"request": {
|
|
428
|
+
"subtype": "hook_callback",
|
|
429
|
+
"callback_id": callback_id,
|
|
430
|
+
"input": {"test": "data"},
|
|
431
|
+
"tool_use_id": None,
|
|
432
|
+
},
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
await query._handle_control_request(request)
|
|
436
|
+
|
|
437
|
+
# Check response has converted field names
|
|
438
|
+
assert len(transport.written_messages) > 0
|
|
439
|
+
last_response = transport.written_messages[-1]
|
|
440
|
+
|
|
441
|
+
response_data = json.loads(last_response)
|
|
442
|
+
result = response_data["response"]["response"]
|
|
443
|
+
|
|
444
|
+
# Verify async_ was converted to async
|
|
445
|
+
assert result.get("async") is True, "async_ should be converted to async"
|
|
446
|
+
assert "async_" not in result, "async_ should not appear in output"
|
|
447
|
+
|
|
448
|
+
# Verify continue_ was converted to continue
|
|
449
|
+
assert result.get("continue") is False, (
|
|
450
|
+
"continue_ should be converted to continue"
|
|
451
|
+
)
|
|
452
|
+
assert "continue_" not in result, "continue_ should not appear in output"
|
|
453
|
+
|
|
454
|
+
# Verify other fields are unchanged
|
|
455
|
+
assert result.get("asyncTimeout") == 10000
|
|
456
|
+
assert result.get("stopReason") == "Testing field conversion"
|
|
457
|
+
assert result.get("systemMessage") == "Fields should be converted"
|
|
458
|
+
|
|
260
459
|
|
|
261
460
|
class TestClaudeAgentOptionsIntegration:
|
|
262
461
|
"""Test that callbacks work through ClaudeAgentOptions."""
|
|
@@ -270,7 +469,7 @@ class TestClaudeAgentOptionsIntegration:
|
|
|
270
469
|
return PermissionResultAllow()
|
|
271
470
|
|
|
272
471
|
async def my_hook(
|
|
273
|
-
input_data:
|
|
472
|
+
input_data: HookInput, tool_use_id: str | None, context: HookContext
|
|
274
473
|
) -> dict:
|
|
275
474
|
return {}
|
|
276
475
|
|
|
@@ -44,13 +44,15 @@ class TestSubprocessCLITransport:
|
|
|
44
44
|
"""Test that cli_path accepts pathlib.Path objects."""
|
|
45
45
|
from pathlib import Path
|
|
46
46
|
|
|
47
|
+
path = Path("/usr/bin/claude")
|
|
47
48
|
transport = SubprocessCLITransport(
|
|
48
49
|
prompt="Hello",
|
|
49
50
|
options=ClaudeAgentOptions(),
|
|
50
|
-
cli_path=
|
|
51
|
+
cli_path=path,
|
|
51
52
|
)
|
|
52
53
|
|
|
53
|
-
|
|
54
|
+
# Path object is converted to string, compare with str(path)
|
|
55
|
+
assert transport._cli_path == str(path)
|
|
54
56
|
|
|
55
57
|
def test_build_command_with_system_prompt_string(self):
|
|
56
58
|
"""Test building CLI command with system prompt as string."""
|
|
@@ -129,19 +131,25 @@ class TestSubprocessCLITransport:
|
|
|
129
131
|
"""Test building CLI command with add_dirs option."""
|
|
130
132
|
from pathlib import Path
|
|
131
133
|
|
|
134
|
+
dir1 = "/path/to/dir1"
|
|
135
|
+
dir2 = Path("/path/to/dir2")
|
|
132
136
|
transport = SubprocessCLITransport(
|
|
133
137
|
prompt="test",
|
|
134
|
-
options=ClaudeAgentOptions(
|
|
135
|
-
add_dirs=["/path/to/dir1", Path("/path/to/dir2")]
|
|
136
|
-
),
|
|
138
|
+
options=ClaudeAgentOptions(add_dirs=[dir1, dir2]),
|
|
137
139
|
cli_path="/usr/bin/claude",
|
|
138
140
|
)
|
|
139
141
|
|
|
140
142
|
cmd = transport._build_command()
|
|
141
|
-
cmd_str = " ".join(cmd)
|
|
142
143
|
|
|
143
|
-
# Check that
|
|
144
|
-
assert "--add-dir
|
|
144
|
+
# Check that both directories are in the command
|
|
145
|
+
assert "--add-dir" in cmd
|
|
146
|
+
add_dir_indices = [i for i, x in enumerate(cmd) if x == "--add-dir"]
|
|
147
|
+
assert len(add_dir_indices) == 2
|
|
148
|
+
|
|
149
|
+
# The directories should appear after --add-dir flags
|
|
150
|
+
dirs_in_cmd = [cmd[i + 1] for i in add_dir_indices]
|
|
151
|
+
assert dir1 in dirs_in_cmd
|
|
152
|
+
assert str(dir2) in dirs_in_cmd
|
|
145
153
|
|
|
146
154
|
def test_session_continuation(self):
|
|
147
155
|
"""Test session continuation options."""
|
|
@@ -322,28 +330,31 @@ class TestSubprocessCLITransport:
|
|
|
322
330
|
from pathlib import Path
|
|
323
331
|
|
|
324
332
|
# Test with string path
|
|
333
|
+
string_path = "/path/to/mcp-config.json"
|
|
325
334
|
transport = SubprocessCLITransport(
|
|
326
335
|
prompt="test",
|
|
327
|
-
options=ClaudeAgentOptions(mcp_servers=
|
|
336
|
+
options=ClaudeAgentOptions(mcp_servers=string_path),
|
|
328
337
|
cli_path="/usr/bin/claude",
|
|
329
338
|
)
|
|
330
339
|
|
|
331
340
|
cmd = transport._build_command()
|
|
332
341
|
assert "--mcp-config" in cmd
|
|
333
342
|
mcp_idx = cmd.index("--mcp-config")
|
|
334
|
-
assert cmd[mcp_idx + 1] ==
|
|
343
|
+
assert cmd[mcp_idx + 1] == string_path
|
|
335
344
|
|
|
336
345
|
# Test with Path object
|
|
346
|
+
path_obj = Path("/path/to/mcp-config.json")
|
|
337
347
|
transport = SubprocessCLITransport(
|
|
338
348
|
prompt="test",
|
|
339
|
-
options=ClaudeAgentOptions(mcp_servers=
|
|
349
|
+
options=ClaudeAgentOptions(mcp_servers=path_obj),
|
|
340
350
|
cli_path="/usr/bin/claude",
|
|
341
351
|
)
|
|
342
352
|
|
|
343
353
|
cmd = transport._build_command()
|
|
344
354
|
assert "--mcp-config" in cmd
|
|
345
355
|
mcp_idx = cmd.index("--mcp-config")
|
|
346
|
-
|
|
356
|
+
# Path object gets converted to string, compare with str(path_obj)
|
|
357
|
+
assert cmd[mcp_idx + 1] == str(path_obj)
|
|
347
358
|
|
|
348
359
|
def test_build_command_with_mcp_servers_as_json_string(self):
|
|
349
360
|
"""Test building CLI command with mcp_servers as JSON string."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/src/claude_agent_sdk/_internal/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{claude_agent_sdk-0.1.1 → claude_agent_sdk-0.1.3}/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
|