Transport¶
The pluggable message delivery layer. Swap transports by changing one line in your topology YAML — agent code is unchanged.
See Transports for a full guide with architecture diagrams.
Protocol¶
civitas.transport.Transport
¶
Bases: Protocol
Protocol that all transports implement.
Five methods. A new transport plugin implements these five methods and the entire Civitas runtime works on it.
start()
async
¶
Initialize connections, bind sockets.
stop()
async
¶
Gracefully close connections, flush pending messages.
subscribe(address, handler)
async
¶
Register a handler for messages arriving at this address.
unsubscribe(address)
async
¶
Remove the handler for an address. No-op if not subscribed.
publish(address, data)
async
¶
Send a message to an address (fire-and-forget).
request(address, data, timeout)
async
¶
Send a message and await a reply (request-reply).
wait_ready()
async
¶
Wait for connections and subscriptions to stabilize. No-op by default.
Transports with slow-joiner problems (e.g. ZMQ PUB/SUB) should override this to sleep or poll until messages will be reliably delivered. Callers invoke this after all subscribe() calls are done but before publishing.
has_reply_address(address)
¶
Return True if address is an active ephemeral reply endpoint.
Ephemeral reply addresses are created by transport.request() and are not registered agents. The bus uses this to route reply messages without going through the Registry.
wait_subscribed(address, timeout=2.0)
async
¶
Block until the subscription for address is effective for peers.
Transports where subscription takes effect synchronously (in-process dict insert) or broker-side (NATS) implement this as a no-op / flush. Transports with asynchronous subscription propagation to publisher sockets (ZMQ PUB/SUB) must confirm propagation — announcing a freshly-subscribed address before its subscription reaches peer PUB sockets makes peers publish into a void (#41).
set_serializer(serializer)
¶
Replace the serializer this transport uses for its OWN internal request()/reply-address bookkeeping (v0.9.2.1 bugfix).
Every transport that implements request() holds its own private
serializer reference, captured at construction, separate from the
MessageBus's. request() needs to inject reply_to into the
message it was given — already-serialized bytes from the bus — which
means deserializing and re-serializing internally. If the bus's
serializer is swapped later (e.g. Runtime.start() activating
message signing) without ALSO swapping the transport's own reference,
that internal round-trip keeps using the stale one: it calls
Message.from_dict() directly on a signed v2 envelope dict
({"v": 2, "msg": {...}, "sig": {...}}), none of which are real
Message fields, silently reconstructing a blank message (found the
hard way: ask() over ZMQ with signing enabled just times out, no
exception, because the resulting blank correlation_id makes the
reply-routing check in AgentProcess._dispatch() silently no-op).
Call this whenever the bus's serializer changes after construction.
Implementations¶
civitas.transport.inprocess.InProcessTransport(serializer, mailbox_size=1000)
¶
Transport for single-process deployments.
Messages are delivered by putting serialized bytes into the recipient's asyncio.Queue. Despite being in-process, messages are still serialized through the configured Serializer to ensure transport-swap compatibility.
Source code in civitas/transport/inprocess.py
wait_ready()
async
¶
request(address, data, timeout)
async
¶
Send a request and await a reply.
Creates a temporary reply address, injects reply_to into the message, publishes the request, and awaits the reply with a timeout.
Source code in civitas/transport/inprocess.py
has_reply_address(address)
¶
set_serializer(serializer)
¶
Replace the serializer used by request()'s internal reply_to round-trip (v0.9.2.1 bugfix). Harmless no-op in effect for this transport specifically — message signing is deliberately skipped for in-process transport by design (D9, same OS process, no wire to protect) — but implemented for interface consistency across all three transports (Transport.set_serializer's docstring has the full story).
Source code in civitas/transport/inprocess.py
wait_subscribed(address, timeout=2.0)
async
¶
civitas.transport.zmq.ZMQTransport(serializer, pub_addr='tcp://127.0.0.1:5559', sub_addr='tcp://127.0.0.1:5560', start_proxy=False, curve_config=None)
¶
Transport for multi-process deployments using ZeroMQ.
Implements the five-method Transport protocol. Messages flow through an XSUB/XPUB proxy for PUB/SUB delivery. Request-reply uses temporary PUB/SUB topics with reply queues, identical to InProcessTransport.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
serializer
|
Serializer
|
Serializer for message encode/decode. |
required |
pub_addr
|
str
|
Address of the proxy XSUB frontend (PUB connects here). |
'tcp://127.0.0.1:5559'
|
sub_addr
|
str
|
Address of the proxy XPUB backend (SUB connects here). |
'tcp://127.0.0.1:5560'
|
start_proxy
|
bool
|
If True, start a ZMQProxy in this process. |
False
|
Source code in civitas/transport/zmq.py
start()
async
¶
Initialize sockets and connect to the proxy.
Source code in civitas/transport/zmq.py
wait_ready()
async
¶
Wait for ZMQ connections and subscriptions to stabilize.
Call after all subscribe() calls are done. Mitigates the ZMQ 'slow joiner' problem where PUB/SUB needs time for the connection handshake and subscription propagation through the proxy.
Source code in civitas/transport/zmq.py
stop()
async
¶
Close sockets, stop proxy, clean up.
Source code in civitas/transport/zmq.py
subscribe(address, handler)
async
¶
Subscribe to messages arriving at this address.
Source code in civitas/transport/zmq.py
unsubscribe(address)
async
¶
Remove a handler and best-effort unsubscribe the SUB socket.
Source code in civitas/transport/zmq.py
publish(address, data)
async
¶
Send a message to an address via PUB/SUB through the proxy.
Same-process reply queues are checked first (short-circuit for local request-reply without going through the proxy).
Source code in civitas/transport/zmq.py
request(address, data, timeout)
async
¶
Send a request and await a reply over PUB/SUB.
Creates a temporary reply topic, subscribes to it, injects reply_to into the message, publishes the request, and awaits the reply.
Source code in civitas/transport/zmq.py
has_reply_address(address)
¶
set_serializer(serializer)
¶
Replace the serializer used by request()'s internal reply_to round-trip (v0.9.2.1 bugfix — see Transport.set_serializer's docstring for the full story: this transport's own serializer reference was never updated when Runtime.start() activated message signing, silently corrupting every ask() into a blank message).
Source code in civitas/transport/zmq.py
wait_subscribed(address, timeout=2.0)
async
¶
Block until the subscription for address has propagated to PUB peers.
ZMQ PUB sockets silently drop messages for topics no subscriber is known for, and subscription propagation (SUB → XPUB → XSUB → every PUB) is asynchronous — measured at ~5–10 ms on Linux and ~20–25 ms on macOS over IPC (#41). Announcing a freshly-subscribed address before propagation completes makes peers publish into a void.
Mechanism: subscribe a throwaway probe topic on the SAME SUB socket
after address was subscribed — same pipe, FIFO, so the probe's
subscription cannot overtake it — then self-publish to the probe topic
until one frame loops back through the proxy. When the probe returns,
the address subscription has propagated at least as far as this
process's own PUB pipe; peer PUB pipes receive the same XSUB broadcast
in parallel (residual window: pipe jitter, sub-millisecond — the full
at-least-once route-establishment guarantee is deferred; see
docs/design/cross-process-spawn.md addendum).
Raises:
| Type | Description |
|---|---|
TimeoutError
|
if the probe does not loop back within |
Source code in civitas/transport/zmq.py
civitas.transport.nats.NATSTransport(serializer, servers='nats://localhost:4222', jetstream=False, stream_name='AGENCY', create_stream_if_missing=True, tls_config=None)
¶
Transport for distributed deployments using NATS.
Implements the five-method Transport protocol. Messages flow through a NATS server. Request-reply uses temporary subscriptions with reply addresses, consistent with InProcess and ZMQ transports.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
serializer
|
Serializer
|
Serializer for message encode/decode. |
required |
servers
|
str | list[str]
|
NATS server URL(s) to connect to. |
'nats://localhost:4222'
|
jetstream
|
bool
|
If True, use JetStream durable subscriptions. |
False
|
stream_name
|
str
|
JetStream stream name (only used if jetstream=True). |
'AGENCY'
|
Source code in civitas/transport/nats.py
start()
async
¶
Connect to the NATS server.
Source code in civitas/transport/nats.py
wait_ready()
async
¶
stop()
async
¶
Disconnect from NATS, clean up subscriptions.
Source code in civitas/transport/nats.py
subscribe(address, handler)
async
¶
Subscribe to messages arriving at this address.
Source code in civitas/transport/nats.py
unsubscribe(address)
async
¶
Remove a handler and best-effort unsubscribe the NATS subscription.
Source code in civitas/transport/nats.py
publish(address, data)
async
¶
Publish a message to an address (fire-and-forget).
Checks local reply queues first (same-process request-reply short-circuit), then publishes via NATS.
Source code in civitas/transport/nats.py
request(address, data, timeout)
async
¶
Send a request and await a reply.
Creates a temporary reply address, subscribes to it, injects reply_to into the message, publishes the request, and awaits the reply. Mirrors the InProcess/ZMQ pattern for consistency.
Source code in civitas/transport/nats.py
has_reply_address(address)
¶
set_serializer(serializer)
¶
Replace the serializer used by request()'s internal reply_to round-trip (v0.9.2.1 bugfix — see Transport.set_serializer's docstring for the full story: this transport's own serializer reference was never updated when Runtime.start() activated message signing, silently corrupting every ask() into a blank message).
Source code in civitas/transport/nats.py
wait_subscribed(address, timeout=2.0)
async
¶
Flush the connection — the broker owns routing, so once the server has processed the SUB (flush round-trips it), peers' publishes are routed.
Source code in civitas/transport/nats.py
Worker¶
civitas.worker.Worker(agents, transport='zmq', zmq_pub_addr='tcp://127.0.0.1:5559', zmq_sub_addr='tcp://127.0.0.1:5560', nats_servers='nats://localhost:4222', nats_jetstream=False, serializer=None, model_provider=None, tool_registry=None, state_store=None, exporters=None, max_restarts=3, components=None)
¶
Hosts agents in a worker process, connecting to an existing broker.
Supports ZMQ (connect to proxy) and NATS (connect to server) transports.
The Worker provides: - Transport connectivity (ZMQ or NATS) - Local registry and message bus - Heartbeat auto-response (handled by AgentProcess._message_loop) - Restart command handling (_agency.restart messages)
Source code in civitas/worker.py
start()
async
¶
Start the worker: connect to proxy, wire agents, start loops.
Source code in civitas/worker.py
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 | |
stop()
async
¶
Stop all agents and disconnect from the proxy.