Skip to content

Message Routing

All messages in Aether are routed by topic address. A sender specifies a target topic string; the gateway validates permissions, publishes the message to the corresponding RabbitMQ Stream, and delivers it to any connected client subscribed to that topic.

Every topic follows a structured format determined by its two-letter prefix:

PrefixTargetFormatDescription
agAgentag::{workspace}::{impl}::{spec}Specific long-running agent instance
tuUnique Tasktu::{workspace}::{impl}::{unique_spec}Named task instance
taAssigned Taskta::{workspace}::{impl}::{task_id}Server-assigned non-unique task instance
tbTask Broadcasttb::{workspace}::{impl}Load-balancing topic; workers compete
usUser (Window)us::{user_id}::{window_id}Specific browser window
uwUser (Workspace)uw::{user_id}::{workspace}User scoped to a workspace
gaGlobal Agentsga::{workspace}Broadcast to all agents in workspace
guGlobal Usersgu::{workspace}Broadcast to all users in workspace
pgProgresspg::{workspace}Progress updates with server-side recipient filtering
eventWorkflow Engineevent::{workspace}Workflow Engine is the sole subscriber
metricMetrics Bridgemetric::receiver0 (senders use metric::*)All workspaces fan-in; workspace in Metric.metadata
svServicesv::{impl}::{spec}Cross-workspace HTTP proxy
brBridgebr::{impl}::{spec}Cross-workspace messaging integration (experimental)

Topic names are validated against allowed prefixes and must be 1–256 characters. Invalid topics return ERR_INVALID_TOPIC.

Every message is wrapped in a server-stamped envelope before being published to RabbitMQ Streams:

message MessageEnvelope {
string source = 1; // Server-verified sender topic (cannot be spoofed)
bytes payload = 2;
MessageType message_type = 3;
int64 timestamp_ms = 4; // Server-assigned timestamp
}

The source field is set by the gateway from the authenticated sender’s identity. Clients cannot inject a false source topic.

TypeDescription
CHATConversational messages between participants (default)
CONTROLSystem control signals (start, stop, configure)
TOOL_CALLTool invocation requests and responses
EVENTBroadcast events routed to the Workflow Engine
METRICTelemetry data routed to the Metrics Bridge

The gateway enforces the permission matrix before publishing any message:

SenderCan Send ToCannot Send To
AgentAgents, Tasks, Users, Events, MetricsOrchestrators, Progress
TaskAgents, Tasks, Users, Events, MetricsOrchestrators, Progress
UserAgents, Tasks, UsersEvents, Metrics, Progress
Workflow EngineAgents, Tasks, Users, Events, Metrics—
Metrics BridgeNone (receive only)All
OrchestratorAgent/Task topics onlyEvents, Metrics
ServiceAgent/Task topics onlyEvents, Metrics, Users, Progress
BridgeEverything (any workspace)—

A SendMessage that violates this matrix returns ERR_PERMISSION_DENIED.

Workspace-scoped principals (agents, tasks, users) cannot send to topics in other workspaces. The workspace component in the target topic must exactly match the sender’s current workspace. Bridges are exempt — cross-workspace routing is their purpose.

Each principal type automatically subscribes to topics on connection. Two subscription modes exist:

Exclusive subscriptions (offset-tracked): A single RabbitMQ consumer reads the stream. Messages are replayed from the last committed offset on reconnection, providing at-least-once delivery even after a disconnect.

Shared subscriptions (no offset): A single RabbitMQ consumer fans out locally to all matching connected clients. No offset tracking — messages sent while no subscriber is connected are not replayed.

PrincipalExclusive (offset-tracked)Shared (no offset)
Agentag::{ws}::{impl}::{spec}ga::{ws}, pg::{ws}
Task (Unique)tu::{ws}::{impl}::{spec}—
Task (Non-Unique)ta::{ws}::{impl}::{id}tb::{ws}::{impl}
Userus::{uid}::{wid}gu::{ws}, uw::{uid}::{ws}, pg::{ws}
Workflow Engineevent::{ws}—
Metrics Bridge—metric::receiver0
Orchestrator—— (task assignments via direct gRPC stream)
Servicesv::{impl}::{spec}—

When a client sends SendMessage:

  1. Gateway verifies the sender’s permission to write to the target topic prefix.
  2. Gateway checks the cross-workspace constraint (target workspace must match sender workspace, unless Bridge).
  3. Authority grant check (if applicable — e.g., a task acting on behalf of a user via an authority grant).
  4. Rate limit check on the sending client.
  5. Message is published to the target RabbitMQ Stream.
  6. If the target is locally connected, the router fans out the message to the connected client immediately.
  7. If the target is not locally connected, the message persists in the stream for replay on reconnection.
  8. If the target is an offline ag:: or tu:: topic, the orchestration trigger fires (see Orchestration & Lazy Loading).

RabbitMQ Streams persist all messages regardless of whether a subscriber is currently connected. For exclusive subscriptions (agents and unique tasks), this means:

  • Messages sent to an offline agent are stored in the stream.
  • When the agent reconnects, it resumes from its last committed consumer offset.
  • All messages sent while it was offline are delivered in order.

This persistence is separate from orchestration. The gateway publishes to the stream first, then optionally triggers orchestration to bring the target online. The agent receives the persisted messages via offset replay when it connects.

Agents and tasks can report progress on their work. The gateway handles ProgressReport upstream messages by:

  1. Validating the sender (only agents/tasks can report progress).
  2. Publishing a ProgressUpdate to pg::{workspace} via RabbitMQ Streams.
  3. Updating the associated task heartbeat in PostgreSQL.

ProgressUpdate messages delivered to subscribers go through a per-client filtering handler that:

  • Suppresses self-echo (sender does not receive its own progress).
  • Applies optional recipient filtering (server-side — only the named recipient sees the update).

Progress updates are also used internally by the gateway to notify parent agents when a task they spawned changes state (running, completed, failed, cancelled). See Orchestration & Lazy Loading for details.

When a client connects, the gateway delivers a ConfigSnapshot before the main message loop begins. This snapshot contains:

  • Workspace KV: All keys in the workspace scope for the agent/task implementation.
  • Global KV: All keys in the global scope for the implementation.
  • Task Context: If the connection was initiated via orchestration, the task metadata and launch parameters.

Users do not receive a ConfigSnapshot.

Users can switch their active workspace mid-connection by sending SwitchWorkspace. The gateway:

  1. Checks ACL for the user’s access to the new workspace.
  2. Unsubscribes from gu::{old_ws}, uw::{uid}::{old_ws}, pg::{old_ws}.
  3. Subscribes to gu::{new_ws}, uw::{uid}::{new_ws}, pg::{new_ws}.
  4. The window-specific subscription us::{uid}::{wid} is unaffected.

If the ACL check fails, the user receives ERR_PERMISSION_DENIED and remains in the current workspace.