diff --git a/src/claude_agent_sdk/_internal/query.py b/src/claude_agent_sdk/_internal/query.py index 7aded938..9c5bd081 100644 --- a/src/claude_agent_sdk/_internal/query.py +++ b/src/claude_agent_sdk/_internal/query.py @@ -398,6 +398,7 @@ async def _handle_control_request(self, request: SDKControlRequest) -> None: ], tool_use_id=permission_request.get("tool_use_id"), agent_id=permission_request.get("agent_id"), + agent_type=permission_request.get("agent_type"), blocked_path=permission_request.get("blocked_path"), decision_reason=permission_request.get("decision_reason"), title=permission_request.get("title"), diff --git a/src/claude_agent_sdk/types.py b/src/claude_agent_sdk/types.py index 5b792644..93cb4736 100644 --- a/src/claude_agent_sdk/types.py +++ b/src/claude_agent_sdk/types.py @@ -212,6 +212,10 @@ class ToolPermissionContext: field-ordering compatibility, so callers do not need to handle ``None``.""" agent_id: str | None = None """If running within the context of a sub-agent, the sub-agent's ID.""" + agent_type: str | None = None + """Agent type name (e.g. "general-purpose", "code-reviewer"). Present + inside a sub-agent (alongside agent_id), or on the main thread of a + session started with --agent (without agent_id).""" blocked_path: str | None = None """The file path that triggered the permission request, if applicable. For example, when a Bash command tries to access a path outside allowed directories.""" @@ -2109,6 +2113,7 @@ class SDKControlPermissionRequest(TypedDict): description: NotRequired[str] tool_use_id: str agent_id: NotRequired[str] + agent_type: NotRequired[str] class SDKControlInitializeRequest(TypedDict): diff --git a/tests/test_tool_callbacks.py b/tests/test_tool_callbacks.py index fce46ec3..7d84bcff 100644 --- a/tests/test_tool_callbacks.py +++ b/tests/test_tool_callbacks.py @@ -301,6 +301,83 @@ async def capture_callback( assert received_context.tool_use_id == "toolu_01XYZ789" assert received_context.agent_id is None + @pytest.mark.anyio + async def test_permission_callback_receives_agent_type(self): + """Test that agent_type is passed through to the context, alongside agent_id.""" + received_context = None + + async def capture_callback( + tool_name: str, input_data: dict, context: ToolPermissionContext + ) -> PermissionResultAllow: + nonlocal received_context + received_context = context + return PermissionResultAllow() + + transport = MockTransport() + query = Query( + transport=transport, + is_streaming_mode=True, + can_use_tool=capture_callback, + hooks=None, + ) + + request = { + "type": "control_request", + "request_id": "test-agenttype", + "request": { + "subtype": "can_use_tool", + "tool_name": "TestTool", + "input": {}, + "permission_suggestions": [], + "tool_use_id": "toolu_01AGENTTYPE", + "agent_id": "agent-456", + "agent_type": "code-reviewer", + }, + } + + await query._handle_control_request(request) + + assert received_context is not None + assert received_context.agent_id == "agent-456" + assert received_context.agent_type == "code-reviewer" + + @pytest.mark.anyio + async def test_permission_callback_missing_agent_type(self): + """Test that agent_type defaults to None when not sent.""" + received_context = None + + async def capture_callback( + tool_name: str, input_data: dict, context: ToolPermissionContext + ) -> PermissionResultAllow: + nonlocal received_context + received_context = context + return PermissionResultAllow() + + transport = MockTransport() + query = Query( + transport=transport, + is_streaming_mode=True, + can_use_tool=capture_callback, + hooks=None, + ) + + request = { + "type": "control_request", + "request_id": "test-noagenttype", + "request": { + "subtype": "can_use_tool", + "tool_name": "TestTool", + "input": {}, + "permission_suggestions": [], + "tool_use_id": "toolu_01XYZ789", + }, + } + + await query._handle_control_request(request) + + assert received_context is not None + assert received_context.agent_type is None + @pytest.mark.anyio async def test_permission_callback_receives_decision_reason(self): """Test that decision_reason and permission-display fields are forwarded