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
4 changes: 4 additions & 0 deletions src/claude_agent_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
PermissionResultAllow,
PermissionResultDeny,
PermissionUpdate,
PostCompactHookInput,
PostCompactHookSpecificOutput,
PostToolUseFailureHookInput,
PostToolUseFailureHookSpecificOutput,
PostToolUseHookInput,
Expand Down Expand Up @@ -539,6 +541,8 @@ async def call_tool(name: str, arguments: dict[str, Any]) -> Any:
"StopHookInput",
"SubagentStopHookInput",
"PreCompactHookInput",
"PostCompactHookInput",
"PostCompactHookSpecificOutput",
"NotificationHookInput",
"SubagentStartHookInput",
"PermissionRequestHookInput",
Expand Down
18 changes: 18 additions & 0 deletions src/claude_agent_sdk/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ class PermissionResultDeny:
| Literal["Stop"]
| Literal["SubagentStop"]
| Literal["PreCompact"]
| Literal["PostCompact"]
| Literal["Notification"]
| Literal["SubagentStart"]
| Literal["PermissionRequest"]
Expand Down Expand Up @@ -309,6 +310,14 @@ class PreCompactHookInput(BaseHookInput):
custom_instructions: str | None


class PostCompactHookInput(BaseHookInput):
"""Input data for PostCompact hook events."""

hook_event_name: Literal["PostCompact"]
trigger: Literal["manual", "auto"]
compact_summary: str


class NotificationHookInput(BaseHookInput):
"""Input data for Notification hook events."""

Expand Down Expand Up @@ -344,6 +353,7 @@ class PermissionRequestHookInput(BaseHookInput, _SubagentContextMixin):
| StopHookInput
| SubagentStopHookInput
| PreCompactHookInput
| PostCompactHookInput
| NotificationHookInput
| SubagentStartHookInput
| PermissionRequestHookInput
Expand Down Expand Up @@ -390,6 +400,13 @@ class SessionStartHookSpecificOutput(TypedDict):
additionalContext: NotRequired[str]


class PostCompactHookSpecificOutput(TypedDict):
"""Hook-specific output for PostCompact events."""

hookEventName: Literal["PostCompact"]
additionalContext: NotRequired[str]


class NotificationHookSpecificOutput(TypedDict):
"""Hook-specific output for Notification events."""

Expand Down Expand Up @@ -417,6 +434,7 @@ class PermissionRequestHookSpecificOutput(TypedDict):
| PostToolUseFailureHookSpecificOutput
| UserPromptSubmitHookSpecificOutput
| SessionStartHookSpecificOutput
| PostCompactHookSpecificOutput
| NotificationHookSpecificOutput
| SubagentStartHookSpecificOutput
| PermissionRequestHookSpecificOutput
Expand Down
57 changes: 57 additions & 0 deletions tests/test_tool_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,63 @@ async def pre_tool_hook(
)
assert result["hookSpecificOutput"]["permissionDecision"] == "allow"

@pytest.mark.asyncio
async def test_post_compact_hook_callback(self):
"""Test that a PostCompact hook callback receives correct input and returns output."""
hook_calls: list[dict[str, Any]] = []

async def post_compact_hook(
input_data: HookInput, tool_use_id: str | None, context: HookContext
) -> HookJSONOutput:
hook_calls.append({"input": input_data, "tool_use_id": tool_use_id})
return {
"hookSpecificOutput": {
"hookEventName": "PostCompact",
"additionalContext": "Compaction logged successfully",
}
}

transport = MockTransport()
query = Query(
transport=transport, is_streaming_mode=True, can_use_tool=None, hooks={}
)

callback_id = "test_post_compact_hook"
query.hook_callbacks[callback_id] = post_compact_hook

request = {
"type": "control_request",
"request_id": "test-post-compact",
"request": {
"subtype": "hook_callback",
"callback_id": callback_id,
"input": {
"session_id": "sess-1",
"transcript_path": "/tmp/t",
"cwd": "/home",
"hook_event_name": "PostCompact",
"trigger": "auto",
"compact_summary": "Conversation summary...",
},
"tool_use_id": None,
},
}

await query._handle_control_request(request)

assert len(hook_calls) == 1
assert hook_calls[0]["input"]["hook_event_name"] == "PostCompact"
assert hook_calls[0]["input"]["trigger"] == "auto"
assert hook_calls[0]["input"]["compact_summary"] == "Conversation summary..."

response_data = json.loads(transport.written_messages[-1])
result = response_data["response"]["response"]
assert result["hookSpecificOutput"]["hookEventName"] == "PostCompact"
assert (
result["hookSpecificOutput"]["additionalContext"]
== "Compaction logged successfully"
)


class TestHookInitializeRegistration:
"""Test that new hook events can be registered through the initialize flow."""
Expand Down
39 changes: 39 additions & 0 deletions tests/test_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
NotificationHookSpecificOutput,
PermissionRequestHookInput,
PermissionRequestHookSpecificOutput,
PostCompactHookInput,
PostCompactHookSpecificOutput,
ResultMessage,
SubagentStartHookInput,
SubagentStartHookSpecificOutput,
Expand Down Expand Up @@ -299,6 +301,34 @@ def test_permission_request_hook_input_with_suggestions(self):
}
assert len(hook_input["permission_suggestions"]) == 1

def test_post_compact_hook_input(self):
"""Test PostCompactHookInput construction."""
hook_input: PostCompactHookInput = {
"session_id": "sess-1",
"transcript_path": "/tmp/transcript.jsonl",
"cwd": "/home/user/project",
"hook_event_name": "PostCompact",
"trigger": "auto",
"compact_summary": "Summary of the compacted conversation...",
}
assert hook_input["hook_event_name"] == "PostCompact"
assert hook_input["trigger"] == "auto"
assert (
hook_input["compact_summary"] == "Summary of the compacted conversation..."
)

def test_post_compact_hook_input_manual_trigger(self):
"""Test PostCompactHookInput with manual trigger."""
hook_input: PostCompactHookInput = {
"session_id": "sess-2",
"transcript_path": "/tmp/transcript.jsonl",
"cwd": "/home/user/project",
"hook_event_name": "PostCompact",
"trigger": "manual",
"compact_summary": "User-initiated compaction summary",
}
assert hook_input["trigger"] == "manual"


class TestHookSpecificOutputTypes:
"""Test hook-specific output type definitions."""
Expand Down Expand Up @@ -345,6 +375,15 @@ def test_post_tool_use_output_has_updated_mcp_tool_output(self):
}
assert output["updatedMCPToolOutput"] == {"result": "modified"}

def test_post_compact_hook_specific_output(self):
"""Test PostCompactHookSpecificOutput construction."""
output: PostCompactHookSpecificOutput = {
"hookEventName": "PostCompact",
"additionalContext": "Compaction complete, summary logged",
}
assert output["hookEventName"] == "PostCompact"
assert output["additionalContext"] == "Compaction complete, summary logged"


class TestMcpServerStatusTypes:
"""Test MCP server status type definitions."""
Expand Down