Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/claude_agent_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
ContentBlock,
ContextUsageCategory,
ContextUsageResponse,
DeferredToolUse,
HookCallback,
HookContext,
HookInput,
Expand Down Expand Up @@ -542,6 +543,7 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
"TaskNotificationStatus",
"TaskUsage",
"ResultMessage",
"DeferredToolUse",
"RateLimitEvent",
"RateLimitInfo",
"RateLimitStatus",
Expand Down
9 changes: 9 additions & 0 deletions src/claude_agent_sdk/_internal/message_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from ..types import (
AssistantMessage,
ContentBlock,
DeferredToolUse,
Message,
MirrorErrorMessage,
RateLimitEvent,
Expand Down Expand Up @@ -221,6 +222,7 @@ def parse_message(data: dict[str, Any]) -> Message | None:

case "result":
try:
deferred = data.get("deferred_tool_use")
return ResultMessage(
subtype=data["subtype"],
duration_ms=data["duration_ms"],
Expand All @@ -235,6 +237,13 @@ def parse_message(data: dict[str, Any]) -> Message | None:
structured_output=data.get("structured_output"),
model_usage=data.get("modelUsage"),
permission_denials=data.get("permission_denials"),
deferred_tool_use=DeferredToolUse(
id=deferred["id"],
name=deferred["name"],
input=deferred["input"],
)
if deferred
else None,
errors=data.get("errors"),
uuid=data.get("uuid"),
)
Expand Down
17 changes: 16 additions & 1 deletion src/claude_agent_sdk/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ class PreToolUseHookSpecificOutput(TypedDict):
"""Hook-specific output for PreToolUse events."""

hookEventName: Literal["PreToolUse"]
permissionDecision: NotRequired[Literal["allow", "deny", "ask"]]
permissionDecision: NotRequired[Literal["allow", "deny", "ask", "defer"]]
permissionDecisionReason: NotRequired[str]
updatedInput: NotRequired[dict[str, Any]]
additionalContext: NotRequired[str]
Expand Down Expand Up @@ -1066,6 +1066,20 @@ class MirrorErrorMessage(SystemMessage):
error: str = ""


@dataclass
class DeferredToolUse:
"""Tool use that was deferred by a PreToolUse hook returning ``"defer"``.

When a PreToolUse hook returns ``permissionDecision: "defer"``, the run
stops and the result message carries the deferred tool call here so the
caller can inspect it and decide whether to resume.
"""

id: str
name: str
input: dict[str, Any]


@dataclass
class ResultMessage:
"""Result message with cost and usage information."""
Expand All @@ -1083,6 +1097,7 @@ class ResultMessage:
structured_output: Any = None
model_usage: dict[str, Any] | None = None
permission_denials: list[Any] | None = None
deferred_tool_use: DeferredToolUse | None = None
errors: list[str] | None = None
uuid: str | None = None

Expand Down
25 changes: 25 additions & 0 deletions tests/test_message_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from claude_agent_sdk._internal.message_parser import parse_message
from claude_agent_sdk.types import (
AssistantMessage,
DeferredToolUse,
RateLimitEvent,
ResultMessage,
ServerToolResultBlock,
Expand Down Expand Up @@ -914,9 +915,33 @@ def test_parse_result_message_optional_fields_absent(self):
assert isinstance(message, ResultMessage)
assert message.model_usage is None
assert message.permission_denials is None
assert message.deferred_tool_use is None
assert message.errors is None
assert message.uuid is None

def test_parse_result_message_with_deferred_tool_use(self):
"""ResultMessage parses deferred_tool_use into a DeferredToolUse."""
data = {
"type": "result",
"subtype": "success",
"duration_ms": 1200,
"duration_api_ms": 900,
"is_error": False,
"num_turns": 1,
"session_id": "session_123",
"deferred_tool_use": {
"id": "toolu_01abc",
"name": "Bash",
"input": {"command": "rm -rf /tmp/scratch"},
},
}
message = parse_message(data)
assert isinstance(message, ResultMessage)
assert isinstance(message.deferred_tool_use, DeferredToolUse)
assert message.deferred_tool_use.id == "toolu_01abc"
assert message.deferred_tool_use.name == "Bash"
assert message.deferred_tool_use.input == {"command": "rm -rf /tmp/scratch"}

def test_parse_result_message_with_errors(self):
"""Test that ResultMessage preserves the errors field from error results.

Expand Down
Loading