Skip to content

Protocols API

SubAgentDepsProtocol

subagents_pydantic_ai.SubAgentDepsProtocol

Bases: Protocol

Protocol for dependencies that support subagent management.

The only method the library calls is clone_for_subagent, which decides what a delegated subagent inherits from its parent. Your deps class may be frozen or use slots -- the library never writes attributes onto it.

Example
Python
@dataclass(slots=True)
class MyDeps:
    backend: Backend

    def clone_for_subagent(self, max_depth: int = 0) -> "MyDeps":
        return MyDeps(backend=self.backend)

Earlier versions also required a subagents: dict[str, Any] attribute. The library never read it, so every application carried a field for nothing; the requirement is gone. A deps class that still declares it satisfies this protocol unchanged.

Source code in src/subagents_pydantic_ai/protocols.py
Python
@runtime_checkable
class SubAgentDepsProtocol(Protocol):
    """Protocol for dependencies that support subagent management.

    The only method the library calls is `clone_for_subagent`, which decides what
    a delegated subagent inherits from its parent. Your deps class may be frozen
    or use `slots` -- the library never writes attributes onto it.

    Example:
        ```python
        @dataclass(slots=True)
        class MyDeps:
            backend: Backend

            def clone_for_subagent(self, max_depth: int = 0) -> "MyDeps":
                return MyDeps(backend=self.backend)
        ```

    Earlier versions also required a `subagents: dict[str, Any]` attribute. The
    library never read it, so every application carried a field for nothing; the
    requirement is gone. A deps class that still declares it satisfies this
    protocol unchanged.
    """

    def clone_for_subagent(self, max_depth: int = 0) -> SubAgentDepsProtocol:
        """Create a new deps instance for a delegated subagent.

        Return a **new** instance rather than `self`. The library relies on each
        delegation getting its own deps, and a shared instance leaks state
        between concurrent subagents.

        A subagent typically inherits shared resources (a backend, a filesystem)
        and gets fresh task-specific state.

        Args:
            max_depth: Remaining nesting budget, one less than the parent's
                `max_nesting_depth`. At or below zero the subagent should not be
                given the means to delegate further. Enforcement is yours: the
                library passes the number through but does not itself refuse a
                nested delegation.

        Returns:
            A new deps instance configured for the subagent.
        """
        ...

clone_for_subagent(max_depth=0)

Create a new deps instance for a delegated subagent.

Return a new instance rather than self. The library relies on each delegation getting its own deps, and a shared instance leaks state between concurrent subagents.

A subagent typically inherits shared resources (a backend, a filesystem) and gets fresh task-specific state.

Parameters:

Name Type Description Default
max_depth int

Remaining nesting budget, one less than the parent's max_nesting_depth. At or below zero the subagent should not be given the means to delegate further. Enforcement is yours: the library passes the number through but does not itself refuse a nested delegation.

0

Returns:

Type Description
SubAgentDepsProtocol

A new deps instance configured for the subagent.

Source code in src/subagents_pydantic_ai/protocols.py
Python
def clone_for_subagent(self, max_depth: int = 0) -> SubAgentDepsProtocol:
    """Create a new deps instance for a delegated subagent.

    Return a **new** instance rather than `self`. The library relies on each
    delegation getting its own deps, and a shared instance leaks state
    between concurrent subagents.

    A subagent typically inherits shared resources (a backend, a filesystem)
    and gets fresh task-specific state.

    Args:
        max_depth: Remaining nesting budget, one less than the parent's
            `max_nesting_depth`. At or below zero the subagent should not be
            given the means to delegate further. Enforcement is yours: the
            library passes the number through but does not itself refuse a
            nested delegation.

    Returns:
        A new deps instance configured for the subagent.
    """
    ...

MessageBusProtocol

subagents_pydantic_ai.MessageBusProtocol

Bases: Protocol

Protocol for message bus implementations.

The message bus enables communication between agents, supporting both fire-and-forget messages and request-response patterns.

Implementations can use different backends: - In-memory (default): Uses asyncio queues - Redis: For distributed multi-process setups - Custom: Any backend implementing this protocol

Example
Python
bus = InMemoryMessageBus()

# Register an agent
queue = bus.register_agent("worker-1")

# Send a message
await bus.send(AgentMessage(
    type=MessageType.TASK_UPDATE,
    sender="parent",
    receiver="worker-1",
    payload={"status": "starting"},
))

# Request-response pattern
response = await bus.ask(
    sender="parent",
    receiver="worker-1",
    question="What is your status?",
    task_id="task-123",
    timeout=30.0,
)
Source code in src/subagents_pydantic_ai/protocols.py
Python
@runtime_checkable
class MessageBusProtocol(Protocol):
    """Protocol for message bus implementations.

    The message bus enables communication between agents, supporting
    both fire-and-forget messages and request-response patterns.

    Implementations can use different backends:
    - In-memory (default): Uses asyncio queues
    - Redis: For distributed multi-process setups
    - Custom: Any backend implementing this protocol

    Example:
        ```python
        bus = InMemoryMessageBus()

        # Register an agent
        queue = bus.register_agent("worker-1")

        # Send a message
        await bus.send(AgentMessage(
            type=MessageType.TASK_UPDATE,
            sender="parent",
            receiver="worker-1",
            payload={"status": "starting"},
        ))

        # Request-response pattern
        response = await bus.ask(
            sender="parent",
            receiver="worker-1",
            question="What is your status?",
            task_id="task-123",
            timeout=30.0,
        )
        ```
    """

    async def send(self, message: AgentMessage) -> None:
        """Send a message to a specific agent.

        Args:
            message: The message to send. Must have a valid receiver.

        Raises:
            KeyError: If the receiver is not registered.
        """
        ...

    async def ask(
        self,
        sender: str,
        receiver: str,
        question: Any,
        task_id: str,
        timeout: float = 30.0,
    ) -> AgentMessage:
        """Send a question and wait for a response.

        This implements a request-response pattern where the sender
        blocks until the receiver answers or the timeout expires.

        Args:
            sender: ID of the asking agent.
            receiver: ID of the agent to ask.
            question: The question payload.
            task_id: Task ID for correlation.
            timeout: Maximum time to wait for response in seconds.

        Returns:
            The response message from the receiver.

        Raises:
            asyncio.TimeoutError: If no response within timeout.
            KeyError: If the receiver is not registered.
        """
        ...

    async def answer(self, original: AgentMessage, answer: Any) -> None:
        """Answer a previously received question.

        Args:
            original: The original question message.
            answer: The answer payload.
        """
        ...

    def register_agent(self, agent_id: str) -> asyncio.Queue[AgentMessage]:
        """Register an agent to receive messages.

        Args:
            agent_id: Unique identifier for the agent.

        Returns:
            A queue where messages for this agent will be delivered.

        Raises:
            ValueError: If agent_id is already registered.
        """
        ...

    def unregister_agent(self, agent_id: str) -> None:
        """Unregister an agent from the message bus.

        After unregistration, messages sent to this agent will raise errors.

        Args:
            agent_id: The agent to unregister.
        """
        ...

    async def get_messages(
        self,
        agent_id: str,
        timeout: float = 0.0,
    ) -> list[AgentMessage]:
        """Get pending messages for an agent.

        Non-blocking retrieval of all pending messages in the agent's queue.
        Optionally waits up to `timeout` seconds for at least one message.

        Args:
            agent_id: The agent to get messages for.
            timeout: Maximum time to wait for a message (0 = no wait).

        Returns:
            List of pending messages (may be empty).

        Raises:
            KeyError: If the agent is not registered.
        """
        ...

send(message) async

Send a message to a specific agent.

Parameters:

Name Type Description Default
message AgentMessage

The message to send. Must have a valid receiver.

required

Raises:

Type Description
KeyError

If the receiver is not registered.

Source code in src/subagents_pydantic_ai/protocols.py
Python
async def send(self, message: AgentMessage) -> None:
    """Send a message to a specific agent.

    Args:
        message: The message to send. Must have a valid receiver.

    Raises:
        KeyError: If the receiver is not registered.
    """
    ...

ask(sender, receiver, question, task_id, timeout=30.0) async

Send a question and wait for a response.

This implements a request-response pattern where the sender blocks until the receiver answers or the timeout expires.

Parameters:

Name Type Description Default
sender str

ID of the asking agent.

required
receiver str

ID of the agent to ask.

required
question Any

The question payload.

required
task_id str

Task ID for correlation.

required
timeout float

Maximum time to wait for response in seconds.

30.0

Returns:

Type Description
AgentMessage

The response message from the receiver.

Raises:

Type Description
TimeoutError

If no response within timeout.

KeyError

If the receiver is not registered.

Source code in src/subagents_pydantic_ai/protocols.py
Python
async def ask(
    self,
    sender: str,
    receiver: str,
    question: Any,
    task_id: str,
    timeout: float = 30.0,
) -> AgentMessage:
    """Send a question and wait for a response.

    This implements a request-response pattern where the sender
    blocks until the receiver answers or the timeout expires.

    Args:
        sender: ID of the asking agent.
        receiver: ID of the agent to ask.
        question: The question payload.
        task_id: Task ID for correlation.
        timeout: Maximum time to wait for response in seconds.

    Returns:
        The response message from the receiver.

    Raises:
        asyncio.TimeoutError: If no response within timeout.
        KeyError: If the receiver is not registered.
    """
    ...

answer(original, answer) async

Answer a previously received question.

Parameters:

Name Type Description Default
original AgentMessage

The original question message.

required
answer Any

The answer payload.

required
Source code in src/subagents_pydantic_ai/protocols.py
Python
async def answer(self, original: AgentMessage, answer: Any) -> None:
    """Answer a previously received question.

    Args:
        original: The original question message.
        answer: The answer payload.
    """
    ...

register_agent(agent_id)

Register an agent to receive messages.

Parameters:

Name Type Description Default
agent_id str

Unique identifier for the agent.

required

Returns:

Type Description
Queue[AgentMessage]

A queue where messages for this agent will be delivered.

Raises:

Type Description
ValueError

If agent_id is already registered.

Source code in src/subagents_pydantic_ai/protocols.py
Python
def register_agent(self, agent_id: str) -> asyncio.Queue[AgentMessage]:
    """Register an agent to receive messages.

    Args:
        agent_id: Unique identifier for the agent.

    Returns:
        A queue where messages for this agent will be delivered.

    Raises:
        ValueError: If agent_id is already registered.
    """
    ...

unregister_agent(agent_id)

Unregister an agent from the message bus.

After unregistration, messages sent to this agent will raise errors.

Parameters:

Name Type Description Default
agent_id str

The agent to unregister.

required
Source code in src/subagents_pydantic_ai/protocols.py
Python
def unregister_agent(self, agent_id: str) -> None:
    """Unregister an agent from the message bus.

    After unregistration, messages sent to this agent will raise errors.

    Args:
        agent_id: The agent to unregister.
    """
    ...

get_messages(agent_id, timeout=0.0) async

Get pending messages for an agent.

Non-blocking retrieval of all pending messages in the agent's queue. Optionally waits up to timeout seconds for at least one message.

Parameters:

Name Type Description Default
agent_id str

The agent to get messages for.

required
timeout float

Maximum time to wait for a message (0 = no wait).

0.0

Returns:

Type Description
list[AgentMessage]

List of pending messages (may be empty).

Raises:

Type Description
KeyError

If the agent is not registered.

Source code in src/subagents_pydantic_ai/protocols.py
Python
async def get_messages(
    self,
    agent_id: str,
    timeout: float = 0.0,
) -> list[AgentMessage]:
    """Get pending messages for an agent.

    Non-blocking retrieval of all pending messages in the agent's queue.
    Optionally waits up to `timeout` seconds for at least one message.

    Args:
        agent_id: The agent to get messages for.
        timeout: Maximum time to wait for a message (0 = no wait).

    Returns:
        List of pending messages (may be empty).

    Raises:
        KeyError: If the agent is not registered.
    """
    ...

The in-memory bus, TaskManager, and DynamicAgentRegistry are documented on their own pages: Message Bus and Registry.

Usage Examples

Implementing SubAgentDepsProtocol

Python
from dataclasses import dataclass, field
from typing import Any

@dataclass
class MyDeps:
    """Custom dependencies implementing SubAgentDepsProtocol."""

    subagents: dict[str, Any] = field(default_factory=dict)
    database_url: str = ""
    api_key: str = ""

    def clone_for_subagent(self, max_depth: int = 0) -> "MyDeps":
        """Create isolated deps for subagent."""
        return MyDeps(
            subagents={} if max_depth <= 0 else self.subagents.copy(),
            database_url=self.database_url,  # Share read-only config
            api_key=self.api_key,
        )

Implementing Custom Message Bus

Python
from subagents_pydantic_ai import MessageBusProtocol, AgentMessage

class RedisMessageBus:
    """Redis-based message bus for distributed systems."""

    def __init__(self, redis_url: str):
        self.redis = Redis.from_url(redis_url)

    async def send(self, message: AgentMessage) -> None:
        channel = f"agent:{message.receiver}"
        await self.redis.publish(channel, message.json())

    async def receive(
        self,
        agent_id: str,
        timeout: float | None = None,
    ) -> AgentMessage | None:
        # Implementation...
        pass

    async def subscribe(self, agent_id: str) -> None:
        pass

    async def unsubscribe(self, agent_id: str) -> None:
        pass

Using TaskManager

Python
from subagents_pydantic_ai import TaskManager, InMemoryMessageBus

bus = InMemoryMessageBus()
manager = TaskManager(message_bus=bus)

# Create a task
handle = await manager.create_task(
    subagent_name="researcher",
    description="Research Python async",
)

# Check status
status = await manager.get_task_status(handle.task_id)

# Answer a question
await manager.answer_question(handle.task_id, "Use asyncio")

# Cancel a task
await manager.cancel_task(handle.task_id, hard=False)

Using DynamicAgentRegistry

Python
from subagents_pydantic_ai import DynamicAgentRegistry

registry = DynamicAgentRegistry()

# List registered agents
agents = registry.list_agents()

# Get a specific agent
agent = registry.get_agent("custom-analyst")

# Remove an agent
registry.remove_agent("custom-analyst")