Skip to content

Communication

Inter-agent messaging: bus, dispatcher, hierarchy resolution, and the multi-agent conversation primitives.

Conversation Enums

enums

Conversation domain enumerations.

ConversationRole

Bases: StrEnum

Author of a single conversational turn.

Attributes:

Name Type Description
USER

A human message into the conversation.

ASSISTANT

A Chief of Staff reply (generic 1:1 + routed paths).

AGENT

A named role agent's group-chat contribution; the author is recorded in the turn's author_agent_id / name fields.

ConversationStatus

Bases: StrEnum

Lifecycle state of a Chief of Staff conversation.

Attributes:

Name Type Description
ACTIVE

Open for further turns; the clarify loop may continue.

PROPOSED

At least one work item proposed into the approval queue from this conversation.

CLOSED

Terminal; no further turns are accepted.

Message

message

Message domain models (see Communication design page).

Part module-attribute

Part = Annotated[TextPart | DataPart | FilePart | UriPart, Discriminator('type')]

Discriminated union of message content parts.

Pydantic uses the type literal field on each part to determine which subtype to deserialize into.

TextPart pydantic-model

Bases: BaseModel

Plain text message content.

Attributes:

Name Type Description
type Literal['text']

Discriminator literal (always "text").

text NotBlankStr

The text content (must not be blank).

Config:

  • frozen: True
  • allow_inf_nan: False
  • extra: forbid

Fields:

type pydantic-field

type = 'text'

Part type discriminator

text pydantic-field

text

Text content

DataPart pydantic-model

Bases: BaseModel

Structured JSON data attached to a message.

The data field is deep-copied and recursively frozen at construction (dict -> MappingProxyType, list -> tuple, set -> frozenset) to preserve the immutability contract of frozen Pydantic models.

Attributes:

Name Type Description
type Literal['data']

Discriminator literal (always "data").

data MappingProxyType[str, object]

Structured JSON content (read-only after construction).

Config:

  • frozen: True
  • allow_inf_nan: False
  • arbitrary_types_allowed: True
  • extra: forbid

Fields:

  • type (Literal['data'])
  • data (MappingProxyType[str, object])

Validators:

  • _deep_copy_and_freeze_data → data

type pydantic-field

type = 'data'

Part type discriminator

data pydantic-field

data

Structured JSON content (read-only)

model_copy

model_copy(*, update=None, deep=False)

Override to re-freeze data when model_copy updates it.

Returns:

Type Description
Self

The copied model, with data re-frozen when the update

Self

replaced it.

Source code in src/synthorg/communication/message.py
@override
def model_copy(
    self,
    *,
    update: Mapping[str, object] | None = None,
    deep: bool = False,
) -> Self:
    """Override to re-freeze data when model_copy updates it.

    Returns:
        The copied model, with ``data`` re-frozen when the update
        replaced it.
    """
    result = super().model_copy(update=update, deep=deep)
    if update and "data" in update:
        raw = update["data"]
        if isinstance(raw, MappingProxyType):
            raw = dict(raw)
        frozen = freeze_recursive(deep_copy_mapping(raw))
        object.__setattr__(result, "data", frozen)
    return result

FilePart pydantic-model

Bases: BaseModel

Reference to a file resource.

Attributes:

Name Type Description
type Literal['file']

Discriminator literal (always "file").

uri NotBlankStr

File URI or path.

mime_type NotBlankStr | None

Optional MIME type of the file.

Config:

  • frozen: True
  • allow_inf_nan: False
  • extra: forbid

Fields:

type pydantic-field

type = 'file'

Part type discriminator

uri pydantic-field

uri

File URI or path

mime_type pydantic-field

mime_type = None

Optional MIME type

UriPart pydantic-model

Bases: BaseModel

Reference to an external URI resource.

Attributes:

Name Type Description
type Literal['uri']

Discriminator literal (always "uri").

uri NotBlankStr

The URI or URL.

Config:

  • frozen: True
  • allow_inf_nan: False
  • extra: forbid

Fields:

type pydantic-field

type = 'uri'

Part type discriminator

uri pydantic-field

uri

URI or URL

MessageMetadata pydantic-model

Bases: BaseModel

Optional metadata carried with a message.

Extends the Communication design page metadata with an additional extra field for arbitrary key-value pairs.

Attributes:

Name Type Description
task_id NotBlankStr | None

Related task identifier.

project_id NotBlankStr | None

Related project identifier.

tokens_used int | None

LLM tokens consumed producing the message.

cost float | None

Estimated cost of the message in the configured currency.

extra tuple[tuple[str, str], ...]

Immutable key-value pairs for arbitrary metadata (extension).

Config:

  • frozen: True
  • allow_inf_nan: False
  • extra: forbid

Fields:

Validators:

  • _validate_extra

task_id pydantic-field

task_id = None

Related task identifier

project_id pydantic-field

project_id = None

Related project identifier

tokens_used pydantic-field

tokens_used = None

LLM tokens consumed

cost pydantic-field

cost = None

Estimated cost in the configured currency

extra pydantic-field

extra = ()

Immutable key-value pairs for arbitrary metadata

Message pydantic-model

Bases: BaseModel

An inter-agent message.

Field schema is based on the Communication design page with typed refinements. The sender field is aliased to "from" for JSON compatibility with the spec format.

Content is represented as a tuple of typed Part objects (text, data, file, URI) rather than a flat string, enabling rich multi-part messages.

Attributes:

Name Type Description
id UUID

Unique message identifier.

timestamp AwareDatetime

When the message was created (must be timezone-aware).

sender NotBlankStr

Agent ID of the sender (aliased to "from" in JSON).

to NotBlankStr

Recipient agent or channel identifier.

type MessageType

Message type classification.

priority MessagePriority

Message priority level.

channel NotBlankStr

Channel the message is sent through.

parts tuple[Part, ...]

Ordered content parts (text, data, files, URIs).

metadata MessageMetadata

Optional message metadata.

Config:

  • frozen: True
  • populate_by_name: True
  • allow_inf_nan: False
  • arbitrary_types_allowed: True

Fields:

id pydantic-field

id

Unique message identifier

timestamp pydantic-field

timestamp

When the message was created (must be timezone-aware)

sender pydantic-field

sender

Sender agent ID

to pydantic-field

to

Recipient agent or channel

type pydantic-field

type

Message type classification

priority pydantic-field

priority = MessagePriority.NORMAL

Message priority level

channel pydantic-field

channel

Channel the message is sent through

parts pydantic-field

parts

Ordered content parts (text, data, files, URIs)

attachments pydantic-field

attachments = ()

Out-of-band parts attached to the message (files, data blobs, URIs) that are not part of the primary content flow. Persisted alongside the message and round-tripped by both backends.

metadata pydantic-field

metadata

Optional message metadata

text property

text

Extract the first TextPart text, or empty string.

Convenience accessor for consumers that only need the primary text content of a message.

Dispatcher

dispatcher

Message dispatcher -- routes incoming messages to registered handlers.

See the Communication design page.

DispatchResult pydantic-model

Bases: BaseModel

Immutable outcome of dispatching a single message.

Attributes:

Name Type Description
message_id UUID

The dispatched message's identifier.

handlers_matched int

Derived count (succeeded + failed).

handlers_succeeded int

Number of handlers that completed without error.

handlers_failed int

Number of handlers that raised an exception.

errors tuple[str, ...]

Error descriptions from failed handlers.

Config:

  • frozen: True
  • allow_inf_nan: False
  • extra: forbid

Fields:

  • message_id (UUID)
  • handlers_succeeded (int)
  • handlers_failed (int)
  • errors (tuple[str, ...])

handlers_matched property

handlers_matched

Total handlers that matched (succeeded + failed).

MessageDispatcher

MessageDispatcher(agent_id='unknown')

Per-agent dispatcher that routes messages to registered handlers.

Parameters:

Name Type Description Default
agent_id str

Identifier of the owning agent (for logging context).

'unknown'
Source code in src/synthorg/communication/dispatcher.py
def __init__(self, agent_id: str = "unknown") -> None:
    self._agent_id = agent_id
    self._registrations: dict[str, HandlerRegistration] = {}

register

register(handler, *, message_types=None, min_priority=LOW, name='unnamed')

Register a handler for incoming messages.

If handler is a bare async function, it is automatically wrapped in a :class:FunctionHandler.

Parameters:

Name Type Description Default
handler MessageHandler | MessageHandlerFunc

The handler instance or async function.

required
message_types frozenset[MessageType] | None

Message types to match (empty/None = all).

None
min_priority MessagePriority

Minimum priority to accept.

LOW
name str

Human-readable label for debugging.

'unnamed'

Returns:

Type Description
str

The unique handler registration ID.

Raises:

Type Description
TypeError

If a MessageHandler instance has a synchronous handle() method.

Source code in src/synthorg/communication/dispatcher.py
def register(
    self,
    handler: MessageHandler | MessageHandlerFunc,
    *,
    message_types: frozenset[MessageType] | None = None,
    min_priority: MessagePriority = MessagePriority.LOW,
    name: str = "unnamed",
) -> str:
    """Register a handler for incoming messages.

    If *handler* is a bare async function, it is automatically wrapped
    in a :class:`FunctionHandler`.

    Args:
        handler: The handler instance or async function.
        message_types: Message types to match (empty/None = all).
        min_priority: Minimum priority to accept.
        name: Human-readable label for debugging.

    Returns:
        The unique handler registration ID.

    Raises:
        TypeError: If a ``MessageHandler`` instance has a synchronous
            ``handle()`` method.
    """
    if not isinstance(handler, MessageHandler):
        handler = FunctionHandler(handler)
    elif not inspect.iscoroutinefunction(handler.handle):
        msg = (
            f"MessageHandler {type(handler).__name__!r} has a "
            f"synchronous handle() -- must be async"
        )
        logger.warning(
            COMM_HANDLER_INVALID,
            agent_id=self._agent_id,
            handler_name=name,
            handler_type=type(handler).__name__,
            error=msg,
        )
        raise TypeError(msg)

    registration = HandlerRegistration(
        handler=handler,
        message_types=message_types or frozenset(),
        min_priority=min_priority,
        name=name,
    )
    self._registrations[registration.handler_id] = registration
    logger.info(
        COMM_HANDLER_REGISTERED,
        agent_id=self._agent_id,
        handler_id=registration.handler_id,
        handler_name=name,
    )
    return registration.handler_id

deregister

deregister(handler_id)

Remove a previously registered handler.

Parameters:

Name Type Description Default
handler_id str

The registration ID returned by :meth:register.

required

Returns:

Type Description
bool

True if the handler was found and removed, False otherwise.

Source code in src/synthorg/communication/dispatcher.py
def deregister(self, handler_id: str) -> bool:
    """Remove a previously registered handler.

    Args:
        handler_id: The registration ID returned by :meth:`register`.

    Returns:
        True if the handler was found and removed, False otherwise.
    """
    removed = self._registrations.pop(handler_id, None)
    if removed is not None:
        logger.info(
            COMM_HANDLER_DEREGISTERED,
            agent_id=self._agent_id,
            handler_id=handler_id,
            handler_name=removed.name,
        )
        return True
    logger.debug(
        COMM_HANDLER_DEREGISTER_MISS,
        agent_id=self._agent_id,
        handler_id=handler_id,
    )
    return False

dispatch async

dispatch(message)

Route a message to all matching handlers concurrently.

Most handler Exception subclasses are isolated; their errors are captured into the per-handler errors slot without affecting siblings. MemoryError and RecursionError are deliberate carve-outs: the interpreter is in catastrophic state, so they re-raise to abort the TaskGroup and cancel all remaining handlers rather than being absorbed silently. BaseException subclasses (e.g. KeyboardInterrupt, asyncio.CancelledError) likewise propagate through the TaskGroup, cancelling all remaining handlers.

Parameters:

Name Type Description Default
message Message

The message to dispatch.

required

Returns:

Name Type Description
A DispatchResult

class:DispatchResult summarising the outcome.

Source code in src/synthorg/communication/dispatcher.py
async def dispatch(self, message: Message) -> DispatchResult:
    """Route a message to all matching handlers concurrently.

    Most handler ``Exception`` subclasses are isolated; their
    errors are captured into the per-handler ``errors`` slot
    without affecting siblings. ``MemoryError`` and
    ``RecursionError`` are deliberate carve-outs: the interpreter
    is in catastrophic state, so they re-raise to abort the
    ``TaskGroup`` and cancel all remaining handlers rather than
    being absorbed silently. ``BaseException`` subclasses (e.g.
    ``KeyboardInterrupt``, ``asyncio.CancelledError``) likewise
    propagate through the ``TaskGroup``, cancelling all remaining
    handlers.

    Args:
        message: The message to dispatch.

    Returns:
        A :class:`DispatchResult` summarising the outcome.
    """
    matched = [
        reg for reg in self._registrations.values() if self._matches(reg, message)
    ]

    logger.debug(
        COMM_DISPATCH_START,
        agent_id=self._agent_id,
        message_id=str(message.id),
        message_type=message.type,
    )

    if not matched:
        logger.debug(
            COMM_DISPATCH_NO_HANDLERS,
            agent_id=self._agent_id,
            message_id=str(message.id),
        )
        return DispatchResult(
            message_id=message.id,
            handlers_succeeded=0,
            handlers_failed=0,
        )

    logger.debug(
        COMM_DISPATCH_HANDLER_MATCHED,
        agent_id=self._agent_id,
        message_id=str(message.id),
        count=len(matched),
    )

    errors: list[str | None] = [None] * len(matched)

    async with asyncio.TaskGroup() as tg:
        for idx, reg in enumerate(matched):
            _ = tg.create_task(
                self._guarded_handle(reg, message, errors, idx),
            )

    error_msgs = tuple(e for e in errors if e is not None)
    succeeded = len(matched) - len(error_msgs)

    logger.info(
        COMM_DISPATCH_COMPLETE,
        agent_id=self._agent_id,
        message_id=str(message.id),
        matched=len(matched),
        succeeded=succeeded,
        failed=len(error_msgs),
    )

    return DispatchResult(
        message_id=message.id,
        handlers_succeeded=succeeded,
        handlers_failed=len(error_msgs),
        errors=error_msgs,
    )

Bus Protocol

bus_protocol

Message bus protocol (see Communication design page).

Defines the swappable interface for message bus backends. The default implementation is :class:InMemoryMessageBus in synthorg.communication.bus.memory.

QuadraticAlertSink

Bases: Protocol

Receives a human-readable alert when a quadratic window is detected.

alert async

alert(*, title, body)

Deliver an operator alert.

Parameters:

Name Type Description Default
title str

Short alert title.

required
body str

Human-readable alert body.

required
Source code in src/synthorg/communication/bus_protocol.py
async def alert(self, *, title: str, body: str) -> None:
    """Deliver an operator alert.

    Args:
        title: Short alert title.
        body: Human-readable alert body.
    """
    ...

MessageBus

Bases: Protocol

Protocol for message bus backends.

All implementations must support the full lifecycle (start/stop), channel management, pub/sub messaging, direct messaging, and channel history.

Uses a pull model: consumers call :meth:receive to get the next message rather than registering push callbacks.

is_running property

is_running

Whether the bus client thinks it is running.

Local state check: flipped by :meth:start / :meth:stop. Does not reflect live server reachability -- use :meth:health_check for that.

start async

start()

Start the bus and create pre-configured channels.

Raises:

Type Description
MessageBusAlreadyRunningError

If the bus is already running.

Source code in src/synthorg/communication/bus_protocol.py
async def start(self) -> None:
    """Start the bus and create pre-configured channels.

    Raises:
        MessageBusAlreadyRunningError: If the bus is already running.
    """
    ...

stop async

stop()

Stop the bus gracefully. Idempotent.

Source code in src/synthorg/communication/bus_protocol.py
async def stop(self) -> None:
    """Stop the bus gracefully.  Idempotent."""
    ...

health_check async

health_check()

Live liveness probe against the underlying transport.

Unlike :attr:is_running, this performs an actual round-trip to confirm the broker is reachable and the connection is healthy. Implementations return False rather than raising when the probe fails, so the caller can treat the bus as degraded without a try/except.

Source code in src/synthorg/communication/bus_protocol.py
async def health_check(self) -> bool:
    """Live liveness probe against the underlying transport.

    Unlike :attr:`is_running`, this performs an actual
    round-trip to confirm the broker is reachable and the
    connection is healthy. Implementations return ``False``
    rather than raising when the probe fails, so the caller
    can treat the bus as degraded without a try/except.
    """
    ...

publish async

publish(message, *, ttl_seconds=None)

Publish a message to its channel.

The target channel is determined by message.channel.

Parameters:

Name Type Description Default
message Message

The message to publish.

required
ttl_seconds float | None

Optional per-message TTL in seconds. When set, the message expires after this duration on the server. Requires the NATS stream to have allow_msg_ttl=True. None (default) defers to stream retention policy.

None

Raises:

Type Description
MessageBusNotRunningError

If the bus is not running.

ChannelNotFoundError

If the target channel does not exist.

Source code in src/synthorg/communication/bus_protocol.py
async def publish(
    self,
    message: Message,
    *,
    ttl_seconds: float | None = None,
) -> None:
    """Publish a message to its channel.

    The target channel is determined by ``message.channel``.

    Args:
        message: The message to publish.
        ttl_seconds: Optional per-message TTL in seconds.  When set,
            the message expires after this duration on the server.
            Requires the NATS stream to have ``allow_msg_ttl=True``.
            ``None`` (default) defers to stream retention policy.

    Raises:
        MessageBusNotRunningError: If the bus is not running.
        ChannelNotFoundError: If the target channel does not exist.
    """
    ...

send_direct async

send_direct(message, *, recipient, ttl_seconds=None)

Send a direct message between two agents.

Lazily creates a DIRECT channel named @{a}:{b} (where a, b are the sorted agent IDs) and subscribes both agents.

Parameters:

Name Type Description Default
message Message

The message to send (message.sender is the sender).

required
recipient NotBlankStr

The recipient agent ID.

required
ttl_seconds float | None

Optional per-message TTL in seconds.

None

Raises:

Type Description
MessageBusNotRunningError

If the bus is not running.

ValueError

If the recipient does not match message.to, an agent ID contains the reserved separator, or the message exceeds the payload limit.

Source code in src/synthorg/communication/bus_protocol.py
async def send_direct(
    self,
    message: Message,
    *,
    recipient: NotBlankStr,
    ttl_seconds: float | None = None,
) -> None:
    """Send a direct message between two agents.

    Lazily creates a DIRECT channel named ``@{a}:{b}`` (where
    a, b are the sorted agent IDs) and subscribes both agents.

    Args:
        message: The message to send (``message.sender`` is the
            sender).
        recipient: The recipient agent ID.
        ttl_seconds: Optional per-message TTL in seconds.

    Raises:
        MessageBusNotRunningError: If the bus is not running.
        ValueError: If the recipient does not match
            ``message.to``, an agent ID contains the reserved
            separator, or the message exceeds the payload limit.
    """
    ...

subscribe async

subscribe(channel_name, subscriber_id)

Subscribe an agent to a channel.

Idempotent -- returns a fresh subscription record if already subscribed (the channel's subscriber list is not duplicated).

Parameters:

Name Type Description Default
channel_name NotBlankStr

Channel to subscribe to.

required
subscriber_id NotBlankStr

Agent ID of the subscriber.

required

Returns:

Type Description
Subscription

The subscription record.

Raises:

Type Description
MessageBusNotRunningError

If the bus is not running.

ChannelNotFoundError

If the channel does not exist.

Source code in src/synthorg/communication/bus_protocol.py
async def subscribe(
    self,
    channel_name: NotBlankStr,
    subscriber_id: NotBlankStr,
) -> Subscription:
    """Subscribe an agent to a channel.

    Idempotent -- returns a fresh subscription record if already
    subscribed (the channel's subscriber list is not duplicated).

    Args:
        channel_name: Channel to subscribe to.
        subscriber_id: Agent ID of the subscriber.

    Returns:
        The subscription record.

    Raises:
        MessageBusNotRunningError: If the bus is not running.
        ChannelNotFoundError: If the channel does not exist.
    """
    ...

unsubscribe async

unsubscribe(channel_name, subscriber_id)

Remove an agent's subscription from a channel.

Parameters:

Name Type Description Default
channel_name NotBlankStr

Channel to unsubscribe from.

required
subscriber_id NotBlankStr

Agent ID to remove.

required

Raises:

Type Description
MessageBusNotRunningError

If the bus is not running.

NotSubscribedError

If the agent is not subscribed.

Source code in src/synthorg/communication/bus_protocol.py
async def unsubscribe(
    self,
    channel_name: NotBlankStr,
    subscriber_id: NotBlankStr,
) -> None:
    """Remove an agent's subscription from a channel.

    Args:
        channel_name: Channel to unsubscribe from.
        subscriber_id: Agent ID to remove.

    Raises:
        MessageBusNotRunningError: If the bus is not running.
        NotSubscribedError: If the agent is not subscribed.
    """
    ...

receive async

receive(channel_name, subscriber_id, *, timeout=None)

Receive the next message from a channel.

Awaits until a message is available, the timeout expires, or the bus is stopped. When timeout is None, awaits indefinitely (or until shutdown).

Parameters:

Name Type Description Default
channel_name NotBlankStr

Channel to receive from.

required
subscriber_id NotBlankStr

Agent ID receiving.

required
timeout float | None

Seconds to wait before returning None.

None

Returns:

Type Description
DeliveryEnvelope | None

A delivery envelope, or None on timeout or shutdown.

Raises:

Type Description
MessageBusNotRunningError

If the bus is not running.

ChannelNotFoundError

If the channel does not exist.

NotSubscribedError

If the subscriber is not subscribed (for TOPIC and DIRECT channels).

Source code in src/synthorg/communication/bus_protocol.py
async def receive(
    self,
    channel_name: NotBlankStr,
    subscriber_id: NotBlankStr,
    *,
    timeout: float | None = None,  # noqa: ASYNC109
) -> DeliveryEnvelope | None:
    """Receive the next message from a channel.

    Awaits until a message is available, the timeout expires, or
    the bus is stopped.  When ``timeout`` is ``None``, awaits
    indefinitely (or until shutdown).

    Args:
        channel_name: Channel to receive from.
        subscriber_id: Agent ID receiving.
        timeout: Seconds to wait before returning ``None``.

    Returns:
        A delivery envelope, or ``None`` on timeout or shutdown.

    Raises:
        MessageBusNotRunningError: If the bus is not running.
        ChannelNotFoundError: If the channel does not exist.
        NotSubscribedError: If the subscriber is not subscribed
            (for TOPIC and DIRECT channels).
    """
    ...

create_channel async

create_channel(channel)

Create a new channel.

Parameters:

Name Type Description Default
channel Channel

Channel definition to create.

required

Returns:

Type Description
Channel

The created channel.

Raises:

Type Description
MessageBusNotRunningError

If the bus is not running.

ChannelAlreadyExistsError

If a channel with that name already exists.

Source code in src/synthorg/communication/bus_protocol.py
async def create_channel(self, channel: Channel) -> Channel:
    """Create a new channel.

    Args:
        channel: Channel definition to create.

    Returns:
        The created channel.

    Raises:
        MessageBusNotRunningError: If the bus is not running.
        ChannelAlreadyExistsError: If a channel with that name
            already exists.
    """
    ...

get_channel async

get_channel(channel_name)

Get a channel by name.

Parameters:

Name Type Description Default
channel_name NotBlankStr

Name of the channel.

required

Returns:

Type Description
Channel

The channel.

Raises:

Type Description
ChannelNotFoundError

If the channel does not exist.

Source code in src/synthorg/communication/bus_protocol.py
async def get_channel(self, channel_name: NotBlankStr) -> Channel:
    """Get a channel by name.

    Args:
        channel_name: Name of the channel.

    Returns:
        The channel.

    Raises:
        ChannelNotFoundError: If the channel does not exist.
    """
    ...

publish_batch async

publish_batch(messages, *, ttl_seconds=None)

Publish multiple messages in a batch.

Optimised for throughput. In NATS backends, uses pipelined async publishes for reduced round-trip overhead.

Parameters:

Name Type Description Default
messages Sequence[Message]

Messages to publish. An empty sequence is a no-op.

required
ttl_seconds float | None

Optional per-message TTL applied to every message in the batch.

None

Raises:

Type Description
MessageBusNotRunningError

If the bus is not running.

ChannelNotFoundError

If any target channel does not exist.

Source code in src/synthorg/communication/bus_protocol.py
async def publish_batch(
    self,
    messages: Sequence[Message],
    *,
    ttl_seconds: float | None = None,
) -> None:
    """Publish multiple messages in a batch.

    Optimised for throughput.  In NATS backends, uses pipelined
    async publishes for reduced round-trip overhead.

    Args:
        messages: Messages to publish.  An empty sequence is a
            no-op.
        ttl_seconds: Optional per-message TTL applied to every
            message in the batch.

    Raises:
        MessageBusNotRunningError: If the bus is not running.
        ChannelNotFoundError: If any target channel does not exist.
    """
    ...

list_channels async

list_channels()

List all channels.

Returns:

Type Description
tuple[Channel, ...]

All registered channels.

Source code in src/synthorg/communication/bus_protocol.py
async def list_channels(self) -> tuple[Channel, ...]:
    """List all channels.

    Returns:
        All registered channels.
    """
    ...

set_quadratic_alert_sink

set_quadratic_alert_sink(sink)

Late-bind an alert sink to the bus's quadratic enforcer, if any.

Boot wiring calls this once the notification dispatcher exists (the bus is built earlier, in the construction phase, so its enforcer starts sink-less). Backends without a quadratic enforcer implement it as a no-op, so wiring never branches on the concrete backend type to reach an enforcer that may not exist.

Source code in src/synthorg/communication/bus_protocol.py
def set_quadratic_alert_sink(self, sink: QuadraticAlertSink) -> None:
    """Late-bind an alert sink to the bus's quadratic enforcer, if any.

    Boot wiring calls this once the notification dispatcher exists (the
    bus is built earlier, in the construction phase, so its enforcer
    starts sink-less). Backends without a quadratic enforcer implement
    it as a no-op, so wiring never branches on the concrete backend
    type to reach an enforcer that may not exist.
    """
    ...

get_channel_history async

get_channel_history(channel_name, *, limit=None)

Get message history for a channel.

Parameters:

Name Type Description Default
channel_name NotBlankStr

Channel to query.

required
limit int | None

Maximum number of most recent messages to return. Values <= 0 return an empty tuple.

None

Returns:

Type Description
tuple[Message, ...]

Messages in chronological order.

Raises:

Type Description
ChannelNotFoundError

If the channel does not exist.

Source code in src/synthorg/communication/bus_protocol.py
async def get_channel_history(
    self,
    channel_name: NotBlankStr,
    *,
    limit: int | None = None,
) -> tuple[Message, ...]:
    """Get message history for a channel.

    Args:
        channel_name: Channel to query.
        limit: Maximum number of most recent messages to return.
            Values ``<= 0`` return an empty tuple.

    Returns:
        Messages in chronological order.

    Raises:
        ChannelNotFoundError: If the channel does not exist.
    """
    ...

Config

config

Communication configuration models (see Communication design page).

MessageRetentionConfig pydantic-model

Bases: BaseModel

Retention settings for channel message history and subscriber delivery.

Attributes:

Name Type Description
max_messages_per_channel int

Maximum messages kept per channel for both in-memory and NATS backends. In-memory backend: collections.deque(maxlen=...). NATS backend: max_msgs_per_subject on the JetStream stream.

max_subscriber_queue_size int

Maximum in-flight envelopes per (channel, subscriber). In-memory backend applies a drop-newest policy with COMM_SUBSCRIBER_QUEUE_OVERFLOW emission when the cap is hit. NATS backend wires this value into ConsumerConfig.max_ack_pending so JetStream pauses delivery to a consumer whose unacked count reaches the cap. The same configuration surface keeps the two backends at parity. Bounded above by :data:_MAX_SUBSCRIBER_QUEUE_SIZE_LIMIT to guard against a misconfiguration or untrusted-config-source DoS exhausting memory at queue creation.

Config:

  • frozen: True
  • allow_inf_nan: False
  • extra: forbid

Fields:

max_messages_per_channel pydantic-field

max_messages_per_channel = 1000

Maximum messages kept per channel

max_subscriber_queue_size pydantic-field

max_subscriber_queue_size = 1024

Maximum in-flight envelopes per (channel, subscriber). In-memory bus: hard cap with drop-newest policy. NATS: ConsumerConfig.max_ack_pending.

NatsConfig pydantic-model

Bases: BaseModel

NATS JetStream backend configuration.

Only applicable when MessageBusConfig.backend == NATS. See docs/design/distributed-runtime.md for stream layout and subject naming.

Attributes:

Name Type Description
url NotBlankStr

NATS server URL (e.g. nats://localhost:4222).

credentials_path str | None

Optional path to a credentials file for secured clusters (creds file or jwt+seed).

stream_name_prefix NotBlankStr

Prefix for JetStream stream names. The bus stream is <prefix>_BUS and the KV bucket for dynamic channels is <prefix>_BUS_CHANNELS.

connect_timeout_seconds float

Seconds to wait for the initial connection before raising.

reconnect_time_wait_seconds float

Seconds between reconnect attempts.

max_reconnect_attempts int

Maximum reconnect attempts before giving up (-1 for unlimited).

publish_ack_wait_seconds float

Seconds to wait for a JetStream publish ack before considering the publish failed.

Config:

  • frozen: True
  • allow_inf_nan: False
  • extra: forbid

Fields:

Validators:

  • _apply_mirrors
  • _validate_url → url

url pydantic-field

url = 'nats://nats:4222'

NATS server URL

credentials_path pydantic-field

credentials_path = None

Optional credentials file path

stream_name_prefix pydantic-field

stream_name_prefix = 'SYNTHORG'

Prefix for JetStream stream names

connect_timeout_seconds pydantic-field

connect_timeout_seconds = 5.0

Initial connect timeout

reconnect_time_wait_seconds pydantic-field

reconnect_time_wait_seconds = 2.0

Seconds between reconnect attempts

max_reconnect_attempts pydantic-field

max_reconnect_attempts = -1

Max reconnect attempts (-1 = unlimited)

publish_ack_wait_seconds pydantic-field

publish_ack_wait_seconds = 5.0

JetStream publish ack wait

health_flush_timeout_seconds pydantic-field

health_flush_timeout_seconds = 2.0

Timeout in seconds for the NATS health-check flush probe. Deliberately tight so a stuck probe fails fast rather than blocking the /healthz endpoint.

QuadraticEnforcementConfig pydantic-model

Bases: BaseModel

O(n^2) message-overhead enforcement settings for the message bus.

Detection compares the windowed inter-agent message count against team_size^2 * quadratic_threshold. The strategy decides what happens when that threshold is crossed (see :class:~synthorg.communication.enums.QuadraticEnforcementStrategy).

Attributes:

Name Type Description
strategy QuadraticEnforcementStrategy

Enforcement mode (default alert_only).

quadratic_threshold float

Fraction of team_size^2 that marks a window as quadratic.

window_seconds float

Sliding window over which inter-agent publishes are counted.

max_agent_connections int

Participant ceiling enforced under hard_block; admitting another agent past this is rejected.

throttle_delay_seconds float

Publish backpressure delay applied under soft_throttle while a window is quadratic.

min_team_size int

Smallest team for which detection runs; below this the quadratic comparison is noise and is skipped.

Config:

  • frozen: True
  • allow_inf_nan: False
  • extra: forbid

Fields:

strategy pydantic-field

strategy = QuadraticEnforcementStrategy.ALERT_ONLY

Quadratic enforcement mode

quadratic_threshold pydantic-field

quadratic_threshold = 0.5

Fraction of team_size^2 that marks a window quadratic

window_seconds pydantic-field

window_seconds = 60.0

Sliding window for counting inter-agent publishes

max_agent_connections pydantic-field

max_agent_connections = 50

Participant ceiling enforced under hard_block

throttle_delay_seconds pydantic-field

throttle_delay_seconds = 0.05

Publish backpressure delay under soft_throttle

min_team_size pydantic-field

min_team_size = 3

Smallest team for which quadratic detection runs

MessageBusConfig pydantic-model

Bases: BaseModel

Message bus backend configuration.

Maps to the Communication design page message_bus.

Attributes:

Name Type Description
backend MessageBusBackend

Transport backend to use.

channels tuple[NotBlankStr, ...]

Pre-defined channel names.

retention MessageRetentionConfig

Message retention settings.

nats NatsConfig | None

NATS-specific configuration (required when backend == NATS, ignored otherwise).

quadratic_enforcement QuadraticEnforcementConfig

O(n^2) message-overhead enforcement.

Config:

  • frozen: True
  • allow_inf_nan: False
  • extra: forbid

Fields:

Validators:

  • _validate_channels
  • _validate_backend_config

backend pydantic-field

backend = MessageBusBackend.INTERNAL

Transport backend

channels pydantic-field

channels = _DEFAULT_CHANNELS

Pre-defined channel names

retention pydantic-field

retention

Message retention settings

nats pydantic-field

nats = None

NATS-specific configuration (required when backend=nats)

quadratic_enforcement pydantic-field

quadratic_enforcement

O(n^2) message-overhead enforcement settings

HierarchyConfig pydantic-model

Bases: BaseModel

Hierarchy enforcement configuration.

Maps to the Communication design page hierarchy.

Attributes:

Name Type Description
enforce_chain_of_command bool

Whether chain-of-command is enforced.

allow_skip_level bool

Whether skip-level messaging is allowed.

Config:

  • frozen: True
  • allow_inf_nan: False
  • extra: forbid

Fields:

enforce_chain_of_command pydantic-field

enforce_chain_of_command = True

Enforce chain-of-command

allow_skip_level pydantic-field

allow_skip_level = False

Allow skip-level messaging

CommunicationConfig pydantic-model

Bases: BaseModel

Top-level communication configuration.

Aggregates the Communication design page sections under a single model.

Attributes:

Name Type Description
default_pattern CommunicationPattern

High-level communication pattern.

message_bus MessageBusConfig

Message bus configuration.

hierarchy HierarchyConfig

Hierarchy enforcement settings.

Config:

  • frozen: True
  • allow_inf_nan: False
  • extra: forbid

Fields:

default_pattern pydantic-field

default_pattern = CommunicationPattern.HYBRID

High-level communication pattern

message_bus pydantic-field

message_bus

Message bus configuration

hierarchy pydantic-field

hierarchy

Hierarchy enforcement settings

Errors

errors

Communication error hierarchy (see Communication design page).

All communication errors carry an immutable context mapping for structured metadata, following the same pattern as ToolError.

CommunicationError

CommunicationError(message, *, context=None)

Bases: DomainError

Base exception for all communication-layer errors.

Attributes:

Name Type Description
message

Human-readable error description.

context MappingProxyType[str, object]

Immutable metadata about the error.

Class Attributes

status_code: HTTP 500 default. error_code: COMMUNICATION_ERROR. error_category: INTERNAL. retryable: False. is_retryable: False (instance-checkable alias of retryable, matching the provider / integration error hierarchies so a retry-decision site can read exc.is_retryable uniformly). default_message: Generic 5xx-safe message.

Initialize a communication error.

Parameters:

Name Type Description Default
message str

Human-readable error description.

required
context Mapping[str, object] | None

Arbitrary metadata about the error. Stored as an immutable mapping; defaults to empty if not provided.

None
Source code in src/synthorg/communication/errors.py
def __init__(
    self,
    message: str,
    *,
    context: Mapping[str, object] | None = None,
) -> None:
    """Initialize a communication error.

    Args:
        message: Human-readable error description.
        context: Arbitrary metadata about the error. Stored as an
            immutable mapping; defaults to empty if not provided.
    """
    self.message = message
    self.context: MappingProxyType[str, object] = MappingProxyType(
        copy.deepcopy(context) if context else {},
    )
    super().__init__(message)

__str__

__str__()

Format error with optional context metadata.

Returns:

Type Description
str

The message, suffixed with (key=value, ...) context when

str

present.

Source code in src/synthorg/communication/errors.py
@override
def __str__(self) -> str:
    """Format error with optional context metadata.

    Returns:
        The message, suffixed with ``(key=value, ...)`` context when
        present.
    """
    if self.context:
        ctx = ", ".join(f"{k}={v!r}" for k, v in self.context.items())
        return f"{self.message} ({ctx})"
    return self.message

ChannelNotFoundError

ChannelNotFoundError(message, *, context=None)

Bases: CommunicationError

Requested channel does not exist.

Source code in src/synthorg/communication/errors.py
def __init__(
    self,
    message: str,
    *,
    context: Mapping[str, object] | None = None,
) -> None:
    """Initialize a communication error.

    Args:
        message: Human-readable error description.
        context: Arbitrary metadata about the error. Stored as an
            immutable mapping; defaults to empty if not provided.
    """
    self.message = message
    self.context: MappingProxyType[str, object] = MappingProxyType(
        copy.deepcopy(context) if context else {},
    )
    super().__init__(message)

ChannelAlreadyExistsError

ChannelAlreadyExistsError(message, *, context=None)

Bases: CommunicationError

Channel with the given name already exists.

Source code in src/synthorg/communication/errors.py
def __init__(
    self,
    message: str,
    *,
    context: Mapping[str, object] | None = None,
) -> None:
    """Initialize a communication error.

    Args:
        message: Human-readable error description.
        context: Arbitrary metadata about the error. Stored as an
            immutable mapping; defaults to empty if not provided.
    """
    self.message = message
    self.context: MappingProxyType[str, object] = MappingProxyType(
        copy.deepcopy(context) if context else {},
    )
    super().__init__(message)

NotSubscribedError

NotSubscribedError(message, *, context=None)

Bases: CommunicationError

Agent is not subscribed to the specified channel.

Source code in src/synthorg/communication/errors.py
def __init__(
    self,
    message: str,
    *,
    context: Mapping[str, object] | None = None,
) -> None:
    """Initialize a communication error.

    Args:
        message: Human-readable error description.
        context: Arbitrary metadata about the error. Stored as an
            immutable mapping; defaults to empty if not provided.
    """
    self.message = message
    self.context: MappingProxyType[str, object] = MappingProxyType(
        copy.deepcopy(context) if context else {},
    )
    super().__init__(message)

MessageBusNotRunningError

MessageBusNotRunningError(message, *, context=None)

Bases: CommunicationError

Operation attempted on a message bus that is not running.

Source code in src/synthorg/communication/errors.py
def __init__(
    self,
    message: str,
    *,
    context: Mapping[str, object] | None = None,
) -> None:
    """Initialize a communication error.

    Args:
        message: Human-readable error description.
        context: Arbitrary metadata about the error. Stored as an
            immutable mapping; defaults to empty if not provided.
    """
    self.message = message
    self.context: MappingProxyType[str, object] = MappingProxyType(
        copy.deepcopy(context) if context else {},
    )
    super().__init__(message)

MessageBusAlreadyRunningError

MessageBusAlreadyRunningError(message, *, context=None)

Bases: CommunicationError

start() called on a message bus that is already running.

Source code in src/synthorg/communication/errors.py
def __init__(
    self,
    message: str,
    *,
    context: Mapping[str, object] | None = None,
) -> None:
    """Initialize a communication error.

    Args:
        message: Human-readable error description.
        context: Arbitrary metadata about the error. Stored as an
            immutable mapping; defaults to empty if not provided.
    """
    self.message = message
    self.context: MappingProxyType[str, object] = MappingProxyType(
        copy.deepcopy(context) if context else {},
    )
    super().__init__(message)

QuadraticConnectionBlockedError

QuadraticConnectionBlockedError(message, *, context=None)

Bases: CommunicationError

A new agent connection is rejected under quadratic hard_block.

Raised by the message bus when the hard_block quadratic enforcement strategy is active and admitting another agent would push the live participant count past max_agent_connections. The category is rate-limit (HTTP 429) so a caller treats it as a capacity rejection to retry later, not a permanent fault.

Source code in src/synthorg/communication/errors.py
def __init__(
    self,
    message: str,
    *,
    context: Mapping[str, object] | None = None,
) -> None:
    """Initialize a communication error.

    Args:
        message: Human-readable error description.
        context: Arbitrary metadata about the error. Stored as an
            immutable mapping; defaults to empty if not provided.
    """
    self.message = message
    self.context: MappingProxyType[str, object] = MappingProxyType(
        copy.deepcopy(context) if context else {},
    )
    super().__init__(message)

HierarchyResolutionError

HierarchyResolutionError(message, *, context=None)

Bases: CommunicationError

Error resolving organizational hierarchy.

Source code in src/synthorg/communication/errors.py
def __init__(
    self,
    message: str,
    *,
    context: Mapping[str, object] | None = None,
) -> None:
    """Initialize a communication error.

    Args:
        message: Human-readable error description.
        context: Arbitrary metadata about the error. Stored as an
            immutable mapping; defaults to empty if not provided.
    """
    self.message = message
    self.context: MappingProxyType[str, object] = MappingProxyType(
        copy.deepcopy(context) if context else {},
    )
    super().__init__(message)

Hierarchy

delegation_types

The delegation-request model engine classification routes through.

A core domain type (it depends only on :mod:core.task and :mod:core.types), so it lives in core rather than communication. Keeping it here lets the engine classification loaders reference a delegation request without importing the communication package hub, which would otherwise close an engine <-> communication cold-import cycle.

DelegationRequest pydantic-model

Bases: BaseModel

Request to delegate a task down the hierarchy.

Attributes:

Name Type Description
delegator_id NotBlankStr

Agent ID of the delegator.

delegatee_id NotBlankStr

Agent ID of the target agent.

task Task

The task to delegate.

refinement str

Additional context from the delegator.

constraints tuple[NotBlankStr, ...]

Extra constraints for the delegatee.

Config:

  • frozen: True
  • allow_inf_nan: False
  • extra: forbid

Fields:

Validators:

  • _validate_self_delegation

delegator_id pydantic-field

delegator_id

Agent ID of the delegator

delegatee_id pydantic-field

delegatee_id

Agent ID of the target agent

task pydantic-field

task

Task to delegate

refinement pydantic-field

refinement = ''

Additional context from the delegator

constraints pydantic-field

constraints = ()

Extra constraints for the delegatee

entity_versions pydantic-field

entity_versions = None

Delegator's known entity version manifest

hierarchy

Hierarchy resolver for organizational structure.

HierarchyResolver

HierarchyResolver(company)

Resolves org hierarchy from a Company structure (read-only).

Built from three sources, in priority order:

  1. Explicit ReportingLine entries (most specific) -- uses subordinate_key/supervisor_key (which resolve to the _id field when set, falling back to the name) and overrides team-derived relationships.
  2. Team.lead for team members
  3. Department.head for team leads without explicit reporting (skipped when head is None)

Detects cycles at construction time.

Parameters:

Name Type Description Default
company Company

Frozen company structure to resolve hierarchy from.

required

Raises:

Type Description
HierarchyResolutionError

If a cycle is detected.

Source code in src/synthorg/communication/hierarchy.py
def __init__(self, company: Company) -> None:
    supervisor_of: dict[str, str] = {}
    reports_of: dict[str, list[str]] = {}

    for dept in company.departments:
        for team in dept.teams:
            # Team lead -> department head (lowest priority)
            if (
                dept.head
                and team.lead != dept.head
                and team.lead not in supervisor_of
            ):
                supervisor_of[team.lead] = dept.head
                reports_of.setdefault(dept.head, []).append(
                    team.lead,
                )

            # Team members -> team lead (medium priority)
            for member in team.members:
                if member == team.lead:
                    continue
                if member not in supervisor_of:
                    supervisor_of[member] = team.lead
                    reports_of.setdefault(team.lead, []).append(member)

        # Explicit reporting lines (highest priority -- override)
        for line in dept.reporting_lines:
            sub_key = line.subordinate_key
            sup_key = line.supervisor_key
            old_sup = supervisor_of.get(sub_key)
            if old_sup == sup_key:
                continue
            if old_sup is not None:
                # Remove from old supervisor's reports
                old_reports = reports_of.get(old_sup, [])
                reports_of[old_sup] = [r for r in old_reports if r != sub_key]
            supervisor_of[sub_key] = sup_key
            reports_of.setdefault(sup_key, []).append(sub_key)

    # Cycle detection
    self._detect_cycles(supervisor_of)

    # Freeze internal state with MappingProxyType
    self._supervisor_of: MappingProxyType[str, str] = MappingProxyType(
        supervisor_of,
    )
    self._reports_of: MappingProxyType[str, tuple[str, ...]] = MappingProxyType(
        {k: tuple(v) for k, v in reports_of.items()}
    )
    self._known_agents: frozenset[str] = frozenset(
        supervisor_of.keys() | supervisor_of.values() | reports_of.keys()
    )

    logger.debug(
        COMM_HIERARCHY_BUILT,
        agents=len(supervisor_of),
        supervisors=len(reports_of),
    )

get_supervisor

get_supervisor(agent_name)

Get the direct supervisor of an agent.

Parameters:

Name Type Description Default
agent_name str

Agent name or ID (matching the key used by ReportingLine entries).

required

Returns:

Type Description
str | None

Supervisor name/ID or None if the agent is at the top.

Source code in src/synthorg/communication/hierarchy.py
def get_supervisor(self, agent_name: str) -> str | None:
    """Get the direct supervisor of an agent.

    Args:
        agent_name: Agent name or ID (matching the key used by
            ``ReportingLine`` entries).

    Returns:
        Supervisor name/ID or ``None`` if the agent is at the top.
    """
    return self._supervisor_of.get(agent_name)

get_direct_reports

get_direct_reports(agent_name)

Get all direct reports of an agent.

Parameters:

Name Type Description Default
agent_name str

Supervisor agent name or ID.

required

Returns:

Type Description
tuple[str, ...]

Tuple of direct report agent names/IDs.

Source code in src/synthorg/communication/hierarchy.py
def get_direct_reports(
    self,
    agent_name: str,
) -> tuple[str, ...]:
    """Get all direct reports of an agent.

    Args:
        agent_name: Supervisor agent name or ID.

    Returns:
        Tuple of direct report agent names/IDs.
    """
    return self._reports_of.get(agent_name, ())

is_direct_report

is_direct_report(supervisor, subordinate)

Check if subordinate directly reports to supervisor.

Parameters:

Name Type Description Default
supervisor str

Supervisor agent name or ID.

required
subordinate str

Potential subordinate agent name or ID.

required

Returns:

Type Description
bool

True if subordinate is a direct report.

Source code in src/synthorg/communication/hierarchy.py
def is_direct_report(
    self,
    supervisor: str,
    subordinate: str,
) -> bool:
    """Check if subordinate directly reports to supervisor.

    Args:
        supervisor: Supervisor agent name or ID.
        subordinate: Potential subordinate agent name or ID.

    Returns:
        True if subordinate is a direct report.
    """
    return subordinate in self.get_direct_reports(supervisor)

is_subordinate

is_subordinate(supervisor, subordinate)

Check if subordinate is anywhere below supervisor.

Walks up the hierarchy from subordinate to root.

Parameters:

Name Type Description Default
supervisor str

Supervisor agent name or ID.

required
subordinate str

Potential subordinate agent name or ID.

required

Returns:

Type Description
bool

True if subordinate is below supervisor at any depth.

Source code in src/synthorg/communication/hierarchy.py
def is_subordinate(
    self,
    supervisor: str,
    subordinate: str,
) -> bool:
    """Check if subordinate is anywhere below supervisor.

    Walks up the hierarchy from subordinate to root.

    Args:
        supervisor: Supervisor agent name or ID.
        subordinate: Potential subordinate agent name or ID.

    Returns:
        True if subordinate is below supervisor at any depth.
    """
    current = subordinate
    while current in self._supervisor_of:
        current = self._supervisor_of[current]
        if current == supervisor:
            return True
    return False

get_ancestors

get_ancestors(agent_name)

Get all ancestors from agent up to root.

Parameters:

Name Type Description Default
agent_name str

Agent name or ID to start from.

required

Returns:

Type Description
str

Tuple of ancestor names/IDs, bottom-up (immediate

...

supervisor first, root last).

Source code in src/synthorg/communication/hierarchy.py
def get_ancestors(self, agent_name: str) -> tuple[str, ...]:
    """Get all ancestors from agent up to root.

    Args:
        agent_name: Agent name or ID to start from.

    Returns:
        Tuple of ancestor names/IDs, bottom-up (immediate
        supervisor first, root last).
    """
    ancestors: list[str] = []
    current = agent_name
    while current in self._supervisor_of:
        current = self._supervisor_of[current]
        ancestors.append(current)
    return tuple(ancestors)

get_lowest_common_manager

get_lowest_common_manager(agent_a, agent_b)

Find the lowest common manager of two agents.

If one agent is an ancestor of the other, that agent is returned as the LCM. When both arguments refer to the same agent, that agent is returned directly.

Parameters:

Name Type Description Default
agent_a str

First agent name or ID.

required
agent_b str

Second agent name or ID.

required

Returns:

Type Description
str | None

Name/ID of the lowest common manager, or None if no

str | None

common manager exists.

Source code in src/synthorg/communication/hierarchy.py
def get_lowest_common_manager(
    self,
    agent_a: str,
    agent_b: str,
) -> str | None:
    """Find the lowest common manager of two agents.

    If one agent is an ancestor of the other, that agent is
    returned as the LCM.  When both arguments refer to the same
    agent, that agent is returned directly.

    Args:
        agent_a: First agent name or ID.
        agent_b: Second agent name or ID.

    Returns:
        Name/ID of the lowest common manager, or ``None`` if no
        common manager exists.
    """
    if agent_a == agent_b:
        if agent_a not in self._known_agents:
            return None
        return agent_a
    ancestors_a = self.get_ancestors(agent_a)
    ancestors_b_set = set(self.get_ancestors(agent_b))
    # Check if agent_a is an ancestor of agent_b
    if agent_a in ancestors_b_set:
        return agent_a
    # Check if agent_b is an ancestor of agent_a
    if agent_b in set(ancestors_a):
        return agent_b
    # Walk agent_a's ancestors bottom-up; first hit in agent_b's
    # ancestor set is the LCM
    for ancestor in ancestors_a:
        if ancestor in ancestors_b_set:
            return ancestor
    return None

get_delegation_depth

get_delegation_depth(from_agent, to_agent)

Get hierarchy levels between two agents.

Parameters:

Name Type Description Default
from_agent str

Higher-level agent name or ID (potential supervisor).

required
to_agent str

Lower-level agent name or ID (potential subordinate).

required

Returns:

Type Description
int | None

Number of hierarchy levels between them, or None if

int | None

to_agent is not below from_agent.

Source code in src/synthorg/communication/hierarchy.py
def get_delegation_depth(
    self,
    from_agent: str,
    to_agent: str,
) -> int | None:
    """Get hierarchy levels between two agents.

    Args:
        from_agent: Higher-level agent name or ID (potential
            supervisor).
        to_agent: Lower-level agent name or ID (potential
            subordinate).

    Returns:
        Number of hierarchy levels between them, or None if
        to_agent is not below from_agent.
    """
    depth = 0
    current = to_agent
    while current in self._supervisor_of:
        current = self._supervisor_of[current]
        depth += 1
        if current == from_agent:
            return depth
    return None

Multi-Agent Conversation

protocol

The contract for invoking an agent inside a multi-party conversation.

AgentCaller is the seam between a conversation and the execution engine: a conversation decides who speaks and with what prompt, and the caller turns that into a real LLM dispatch. Keeping it a callable rather than a service means a conversation never imports the engine layer.

AgentCaller module-attribute

AgentCaller = Callable[[str, str, int, str], Awaitable[AgentResponse]]

Callback to invoke an agent during a conversation.

Signature: (agent_id, prompt, max_tokens, conversation_id) -> AgentResponse

conversation_id is threaded through so cost-recording attribution carries the real conversation identifier per turn instead of a synthetic placeholder.

models

What one agent returned from a single invocation.

AgentResponse pydantic-model

Bases: BaseModel

Result of a single agent invocation.

Attributes:

Name Type Description
agent_id NotBlankStr

Identifier of the agent that responded.

content str

Text content of the response.

input_tokens int

Tokens consumed by the prompt.

output_tokens int

Tokens generated in the response.

cost float

Estimated cost of the invocation.

Config:

  • frozen: True
  • allow_inf_nan: False
  • extra: forbid

Fields:

agent_id pydantic-field

agent_id

Agent that responded

content pydantic-field

content

Response content

input_tokens pydantic-field

input_tokens = 0

Prompt tokens consumed

output_tokens pydantic-field

output_tokens = 0

Response tokens generated

cost pydantic-field

cost = 0.0

Estimated invocation cost

token_tracker

Per-round token budget for a multi-party conversation.

Concurrency note: TokenTracker is safe for use within a single asyncio event loop (cooperative multitasking). record() runs to completion without suspension, so concurrent coroutines sharing a tracker will not interleave reads and writes. However, intermediate values of remaining during a parallel TaskGroup phase reflect only the tasks that have completed so far -- callers should pre-divide budgets before launching parallel work rather than checking remaining inside concurrent tasks.

TokenTracker

TokenTracker(*, budget)

Token budget scoped to a single conversation run.

Consumption only ever grows, and only through :meth:record, which is where the non-negative check lives. The totals are therefore read through properties rather than exposed as writable attributes: an assignment could otherwise move a tally backwards and leave every later remaining reading a budget nothing spent.

Open a run with budget tokens to spend.

Parameters:

Name Type Description Default
budget int

Total token budget for the conversation.

required

Raises:

Type Description
ValueError

If budget is not positive.

Source code in src/synthorg/communication/multi_agent/token_tracker.py
def __init__(self, *, budget: int) -> None:
    """Open a run with *budget* tokens to spend.

    Args:
        budget: Total token budget for the conversation.

    Raises:
        ValueError: If ``budget`` is not positive.
    """
    if budget <= 0:
        msg = f"budget must be positive, got {budget}"
        raise ValueError(msg)
    self._budget = budget
    self._input_tokens = 0
    self._output_tokens = 0

budget property

budget

Total token budget for the conversation.

input_tokens property

input_tokens

Total prompt tokens consumed so far.

output_tokens property

output_tokens

Total response tokens generated so far.

used property

used

Total tokens consumed so far.

remaining property

remaining

Tokens remaining in the budget.

is_exhausted property

is_exhausted

Whether the budget is fully consumed.

record

record(input_tokens, output_tokens)

Record token usage from an agent call.

Logs a warning when token usage exceeds the budget after recording.

Parameters:

Name Type Description Default
input_tokens int

Prompt tokens consumed (must be >= 0).

required
output_tokens int

Response tokens generated (must be >= 0).

required

Raises:

Type Description
ValueError

If either token count is negative.

Source code in src/synthorg/communication/multi_agent/token_tracker.py
def record(self, input_tokens: int, output_tokens: int) -> None:
    """Record token usage from an agent call.

    Logs a warning when token usage exceeds the budget after
    recording.

    Args:
        input_tokens: Prompt tokens consumed (must be >= 0).
        output_tokens: Response tokens generated (must be >= 0).

    Raises:
        ValueError: If either token count is negative.
    """
    if input_tokens < 0 or output_tokens < 0:
        msg = (
            f"Token counts must be non-negative, got "
            f"input_tokens={input_tokens}, "
            f"output_tokens={output_tokens}"
        )
        logger.warning(
            MULTI_AGENT_VALIDATION_FAILED,
            error=msg,
            input_tokens=input_tokens,
            output_tokens=output_tokens,
        )
        raise ValueError(msg)
    self._input_tokens += input_tokens
    self._output_tokens += output_tokens

    if self.used > self._budget:
        logger.warning(
            MULTI_AGENT_BUDGET_EXHAUSTED,
            tokens_used=self.used,
            token_budget=self._budget,
            overage=self.used - self._budget,
        )

__repr__

__repr__()

Return the budget and what has been spent against it.

Returns:

Type Description
str

A TokenTracker(...) string carrying budget and totals.

Source code in src/synthorg/communication/multi_agent/token_tracker.py
@override
def __repr__(self) -> str:
    """Return the budget and what has been spent against it.

    Returns:
        A ``TokenTracker(...)`` string carrying budget and totals.
    """
    return (
        f"TokenTracker(budget={self._budget}, "
        f"input_tokens={self._input_tokens}, "
        f"output_tokens={self._output_tokens})"
    )

agent_caller

:data:AgentCaller factory for multi-party conversations.

A conversation invokes agents through an :data:AgentCaller callable. :func:build_agent_caller composes an agent registry (for identity lookup) with a :class:ProviderRegistry (for LLM dispatch) and runs one provider.complete() call per invocation.

One turn is one LLM call. Sequencing turns belongs to the conversation; this module runs a single agent's inference.

UnknownConversationAgentError

UnknownConversationAgentError(agent_id)

Bases: NotFoundError

Raised when a conversation invokes an unregistered agent.

Attributes:

Name Type Description
agent_id NotBlankStr

The agent identifier that was not found in the registry.

Raises:

Type Description
ValueError

If agent_id is blank. The annotation alone binds only inside a Pydantic model.

Source code in src/synthorg/communication/multi_agent/agent_caller.py
def __init__(self, agent_id: NotBlankStr) -> None:
    require_not_blank(agent_id, "agent_id")
    super().__init__(
        f"Agent {agent_id!r} is not registered in the agent registry; "
        f"cannot dispatch LLM call"
    )
    self.agent_id: NotBlankStr = agent_id

build_agent_caller

build_agent_caller(*, agent_registry, provider_registry, cost_tracker=None)

Construct an :data:AgentCaller backed by real services.

Parameters:

Name Type Description Default
agent_registry AgentRegistryProtocol

Source of truth for agent identities.

required
provider_registry ProviderRegistry

Source of truth for LLM providers.

required
cost_tracker CostTrackerProtocol | None

Optional cost tracker; when wired each turn records via the chokepoint.

None

Returns:

Type Description
AgentCaller

An async callback matching the :data:AgentCaller contract.

Source code in src/synthorg/communication/multi_agent/agent_caller.py
def build_agent_caller(
    *,
    agent_registry: AgentRegistryProtocol,
    provider_registry: ProviderRegistry,
    cost_tracker: CostTrackerProtocol | None = None,
) -> AgentCaller:
    """Construct an :data:`AgentCaller` backed by real services.

    Args:
        agent_registry: Source of truth for agent identities.
        provider_registry: Source of truth for LLM providers.
        cost_tracker: Optional cost tracker; when wired each turn
            records via the chokepoint.

    Returns:
        An async callback matching the :data:`AgentCaller` contract.
    """

    async def _caller(
        agent_id: str,
        prompt: str,
        max_tokens: int,
        conversation_id: str,
    ) -> AgentResponse:
        """Invoke the agent's provider for one turn.

        Returns:
            The agent's response with token usage and cost.

        Raises:
            UnknownConversationAgentError: If the agent id is not registered.
            ValueError: If either identifier is blank. ``NotBlankStr`` only
                binds inside a Pydantic model, so both are checked here or
                a blank one reaches the registry lookup and the cost row
                unexamined.
        """
        typed_agent_id = require_not_blank(agent_id, "agent_id")
        cleaned_conversation_id = require_not_blank(
            conversation_id.strip(),
            "conversation_id",
        )
        logger.info(
            MULTI_AGENT_CALLED,
            agent_id=agent_id,
            conversation_id=cleaned_conversation_id,
            max_tokens=max_tokens,
            prompt_length=len(prompt),
        )
        identity = await agent_registry.get(typed_agent_id)
        if identity is None:
            logger.warning(
                MULTI_AGENT_CALL_FAILED,
                agent_id=agent_id,
                conversation_id=cleaned_conversation_id,
                error_type="UnknownConversationAgentError",
            )
            raise UnknownConversationAgentError(typed_agent_id)

        provider_name = str(identity.model.provider)
        provider = provider_registry.get(provider_name)
        messages = _build_messages(identity, prompt)
        # The conversation's own cap still binds; the agent's binding only
        # tightens it further when an operator set one, and answers nothing
        # when unset.
        own = identity.model.max_tokens
        effective_max_tokens = max_tokens if own is None else min(max_tokens, own)
        # Re-validated rather than copied in: `model_copy(update=...)` skips
        # validation by design, and this cap is computed from a caller-supplied
        # budget while `CompletionConfig.max_tokens` is `gt=0`, so a copy would
        # hand a non-positive ceiling straight to the provider.
        config = CompletionConfig.model_validate(
            resolve_sampling(identity).model_dump()
            | {"max_tokens": effective_max_tokens}
        )
        try:
            async with cost_recording_scope(
                cost_tracker=cost_tracker,
                agent_id=typed_agent_id,
                # Multi-agent coordination is not a registered system
                # prompt class.
                purpose=None,
                call_category=LLMCallCategory.COORDINATION,
            ):
                response = await provider.complete(
                    messages,
                    str(identity.model.model_id),
                    config=config,
                )
        except Exception as exc:
            reraise_critical(exc)
            logger.warning(
                MULTI_AGENT_CALL_FAILED,
                agent_id=agent_id,
                provider=provider_name,
                error_type=type(exc).__name__,
                error=safe_error_description(exc),
            )
            raise
        agent_response = AgentResponse(
            agent_id=typed_agent_id,
            content=response.content or "",
            input_tokens=response.usage.input_tokens,
            output_tokens=response.usage.output_tokens,
            cost=response.usage.cost,
        )
        logger.info(
            MULTI_AGENT_RESPONDED,
            agent_id=agent_id,
            input_tokens=agent_response.input_tokens,
            output_tokens=agent_response.output_tokens,
            cost=agent_response.cost,
        )
        return agent_response

    return _caller