Here is the refactored package structure. Each file follows the Single Responsibility Principle, uses proper relative imports, and maintains a clean public API.

### `queue/__init__.py`
```python
"""
Job Queue Package
Public API exports for external consumers.
"""
from .core import Job, JobQueue
from .priority import PriorityQueue
from .retry import process_with_retry

__all__ = ["Job", "JobQueue", "PriorityQueue", "process_with_retry"]
```

### `queue/core.py`
```python
"""
Core data models and basic FIFO queue operations.
"""
from __future__ import annotations

import uuid
from collections import deque
from dataclasses import dataclass, field
from typing import Any, Optional


@dataclass
class Job:
    """Represents a unit of work to be processed."""
    id: str = field(default_factory=lambda: str(uuid.uuid4()))
    payload: Any = None
    priority: int = 0
    status: str = "pending"
    retries: int = 0
    max_retries: int = 3


class JobQueue:
    """Basic FIFO job queue implementation."""
    
    def __init__(self) -> None:
        self._queue: deque[Job] = deque()

    def enqueue(self, job: Job) -> None:
        """Add a job to the end of the queue."""
        self._queue.append(job)

    def dequeue(self) -> Optional[Job]:
        """Remove and return the next job from the queue."""
        if self._queue:
            job = self._queue.popleft()
            job.status = "processing"
            return job
        return None

    def is_empty(self) -> bool:
        """Check if the queue contains no jobs."""
        return len(self._queue) == 0

    def size(self) -> int:
        """Return the number of jobs in the queue."""
        return len(self._queue)
```

### `queue/priority.py`
```python
"""
Priority queue implementation using a binary heap.
"""
import heapq
from typing import Optional

from .core import Job


class PriorityQueue:
    """Priority-based job queue. Lower priority values are processed first."""
    
    def __init__(self) -> None:
        self._heap: list[tuple[int, int, Job]] = []
        self._counter: int = 0  # Tie-breaker for stable sorting

    def enqueue(self, job: Job) -> None:
        """Add a job to the priority queue."""
        # Format: (priority, insertion_order, job)
        heapq.heappush(self._heap, (job.priority, self._counter, job))
        self._counter += 1

    def dequeue(self) -> Optional[Job]:
        """Remove and return the highest-priority job."""
        if self._heap:
            _, _, job = heapq.heappop(self._heap)
            job.status = "processing"
            return job
        return None

    def is_empty(self) -> bool:
        """Check if the priority queue contains no jobs."""
        return len(self._heap) == 0

    def size(self) -> int:
        """Return the number of jobs in the priority queue."""
        return len(self._heap)
```

### `queue/retry.py`
```python
"""
Retry logic with exponential backoff for job processing.
"""
import logging
import time
from typing import Callable, Any

from .core import Job

logger = logging.getLogger(__name__)


def process_with_retry(job: Job, process_func: Callable[[Job], Any]) -> Any:
    """
    Execute a job with automatic retries and exponential backoff.
    
    Args:
        job: The job instance to process.
        process_func: Callable that accepts a Job and returns a result.
        
    Returns:
        The result of the successful process_func call.
        
    Raises:
        Exception: The last exception raised if all retries are exhausted.
    """
    last_exception: Exception | None = None
    
    for attempt in range(job.max_retries + 1):
        try:
            job.status = "processing"
            result = process_func(job)
            job.status = "completed"
            return result
        except Exception as e:
            last_exception = e
            job.retries += 1
            job.status = "retrying"
            
            if attempt < job.max_retries:
                backoff_time = min(2 ** attempt, 60)  # Cap at 60s to prevent extreme delays
                logger.warning(
                    "Job %s failed (attempt %d/%d). Retrying in %.1fs... Error: %s",
                    job.id, attempt + 1, job.max_retries + 1, backoff_time, e
                )
                time.sleep(backoff_time)
            else:
                job.status = "failed"
                logger.error(
                    "Job %s permanently failed after %d attempts. Error: %s",
                    job.id, job.max_retries + 1, e
                )
                raise last_exception  # type: ignore[misc]
```

### Key Design Decisions:
1. **Single Responsibility**: `core.py` handles data modeling and basic FIFO behavior. `priority.py` focuses solely on heap-based ordering. `retry.py` encapsulates fault-tolerance logic.
2. **Interface Consistency**: Both `JobQueue` and `PriorityQueue` share the same public methods (`enqueue`, `dequeue`, `is_empty`, `size`), allowing them to be swapped without changing consumer code.
3. **Safe Backoff**: Added a `min(..., 60)` cap to prevent runaway sleep times in production while preserving exponential growth.
4. **Clean Imports**: Uses relative imports (`.core`, `.priority`, `.retry`) which is the standard for Python packages. Circular dependencies are avoided since `core.py` has zero internal imports.
5. **Type Hints & Docstrings**: Full type annotations and documentation for maintainability and IDE support.