Routing and delivery
Messaging is the vocabulary the rest of AgentLane builds on. Before there are tools, handoffs, or runs, there are only recipients, topics, envelopes, and delivery outcomes.
That vocabulary is represented directly in code:
AgentId names one recipient,
TopicId names a publish target,
MessageEnvelope carries the
work, and DeliveryOutcome or
PublishAck tell the caller what
happened. Subscription matching is governed by
SubscriptionKind
(TYPE_EXACT / TYPE_PREFIX), and the post-match delivery lifecycle is
governed by DeliveryMode.
Two Delivery Patterns
There are two caller-facing ways to move work:
send_message(...)for one recipient and a terminal outcomepublish_message(...)for topic-based fan-out and enqueue confirmation
Use send when the caller needs completion. Use publish when the caller needs to announce an event and let matching subscribers process it independently.
Identities And Topics
An AgentId points at one
runtime recipient. It is made from an AgentType and an AgentKey.
A TopicId describes a publish
target instead. It stores a topic type and a source, where route_key is
exposed as a read-only property aliasing source. The route key is what later
lets publish deliveries preserve stateful affinity for the same logical stream
of work. Build one with TopicId.from_values(type_value=..., route_key=...) or
the Topics convenience
constructor Topics.id(type_value, route_key).
Once a delivery is created, it travels as a
MessageEnvelope. The envelope
holds the sender, recipient or topic, the
Payload, and correlation data
that must survive transport. A Payload carries a schema_name,
content_type, a PayloadFormat
(JSON / PROTOBUF / BYTES), and the application data. The envelope’s
MessageKind
(RPC_REQUEST / RPC_RESPONSE / PUBLISH_EVENT) records whether it is a
direct send or a published event.
Correlation and identity primitives are typed value wrappers:
CorrelationId,
IdempotencyKey, and
MessageId each expose a .new()
factory that mints a fresh identifier.
Direct Send
send_message(...) resolves one recipient, enqueues one delivery, and waits for
one terminal
DeliveryOutcome.
That outcome is where the caller learns how the message resolved. The status is
a DeliveryStatus, which has eight
terminal states:
DELIVERED— handler completed successfully.DROPPED— message was intentionally discarded by runtime policy.UNDELIVERABLE— recipient could not be resolved/created, so dispatch never reached a handler.TIMEOUT— delivery exceeded the configured processing deadline.CANCELED— delivery was canceled (for example during shutdown or cooperative cancellation).HANDLER_ERROR— handler resolution or execution raised an exception.SERIALIZATION_ERROR— payload serialization/deserialization failed before the handler ran.POLICY_REJECTED— runtime policy rejected dispatch before execution.
Publish
publish_message(...) starts from one topic and expands it into one or more
concrete deliveries. The publish side returns a
PublishAck, which tells you how
many recipients were enqueued. It does not tell you whether those recipients
finished their handlers successfully.
That distinction matters when you design workflows. Publish is a fan-out mechanism, not a multi-recipient RPC.
Subscriptions And Delivery Modes
A Subscription maps a topic
match to an agent type. Two decisions shape it: how a topic is matched, and how
matched deliveries reuse recipients.
Matching: SubscriptionKind
SubscriptionKind selects the
matching strategy:
TYPE_EXACTmatches only whentopic.typeexactly equals the pattern.TYPE_PREFIXmatches whentopic.typestarts with the pattern.
Create subscriptions through the convenience constructors Subscription.exact
and Subscription.prefix, or register them directly on the runtime with
runtime.subscribe_exact(topic_type=..., agent_type=...) and
runtime.subscribe_prefix(topic_prefix=..., agent_type=...).
Delivery: DeliveryMode
After a subscription matches, DeliveryMode controls the delivery lifecycle.
The main choice is whether publish deliveries should reuse a stateful recipient
or create a fresh one.
DeliveryMode.STATEFUL uses
the topic route key to derive a stable recipient key. That means repeated events
for the same route key reach the same cached agent instance.
DeliveryMode.STATELESS
creates a unique recipient key per delivery. That is useful for fan-out work
where instance reuse is not part of the contract.
Routing Policy
The RoutingEngine evaluates active
subscriptions against a published topic through a
RoutingPolicy. The default
SourceKeyAffinityRoutingPolicy
maps each matched topic’s route key onto the recipient agent key, then dedups
and orders the resulting PublishRoute list deterministically: stateful routes
are deduped by concrete recipient id, stateless routes by subscription plus
recipient type, and both are stably sorted so fan-out is reproducible across
runs.
Ordering And Correlation
Ordering is guaranteed per recipient. If multiple deliveries target the same
AgentId, they are processed FIFO. Different recipients may run concurrently.
Correlation is a separate concern. Preserve correlation_id when work should be
traceable across multiple hops. Use idempotency_key when retries need
deduplication semantics at the transport or runtime boundary.
By default the runtime generates the envelope message_id. Pass an explicit
message_id to send_message(...) when the sender must know the id before
delivery — for example to bridge trace context across a hop, where the sender
keys a snapshot under the id and the receiver looks the same id up via
MessageContext.message_id.
Example
This snippet shows the API shape. It registers a subscription but no agent
factory for the worker type, so the stateful-affinity behavior described above
is not exercised here — a real run requires a registered agent factory for the
recipient type. The status assertion therefore lists a representative subset of
the DeliveryStatus states rather than asserting a single result.
from agentlane.messaging import AgentId, DeliveryMode, TopicId
from agentlane.runtime import SingleThreadedRuntimeEngine
runtime = SingleThreadedRuntimeEngine()
runtime.subscribe_exact(
topic_type="workflow.plan_ready",
agent_type="worker",
delivery_mode=DeliveryMode.STATEFUL,
)
ack = await runtime.publish_message(
{"plan_id": "p-1"},
topic=TopicId.from_values(
type_value="workflow.plan_ready",
route_key="session-42",
),
)
result = await runtime.send_message(
{"task_id": "t-1"},
recipient=AgentId.from_values("worker", "session-42"),
)
assert ack.enqueued_recipient_count >= 0
assert result.status.value in {
"delivered",
"handler_error",
"policy_rejected",
"undeliverable",
}