Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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