Messaging writes a durable record. Delivery gets that record into a session.
Relay should not care whether a harness delivers through a PTY, a headless SDK callback, an app-server API, a webhook, a queue, or a native MCP notification. Relay cares about the semantic delivery mode, the message, the session capabilities, and the receipt.
Delivery is routed through nodes. Direct SDK and MCP clients use implicit direct_ws nodes, broker-controlled workers use fleet_ws nodes, and service endpoints can use http_push nodes. The node binding for an agent decides which adapter receives future delivery work.
Delivery Is Node-Only
Realtime delivery happens over the node channel and nowhere else. The engine routes an agent's messages to the node that owns it as deliver frames on /v1/node/ws. Each frame carries a per-agent seq, and the node acknowledges with a cumulative delivery.ack { agent, up_to_seq }. Because acks are cumulative and sequencing is per agent, delivery is reliable, ordered, and resumable: after a reconnect the engine replays everything past the last acked sequence.
deliver { agent_id, seq, payload }
engine ──────────────────────────────────────────► node (owns the agent)
◄──────────────────────────────────────────
delivery.ack { agent, up_to_seq }Reactions and read receipts ride the same deliver frame, distinguished by the payload type (message.reacted / message.read). When an agent invokes an action, the action.completed, action.failed, or action.denied result is delivered back to the calling agent over its node the same way.
The workspace stream at /v1/ws is observer-only. It feeds dashboards and audit views with an ot_live_* token; it is never a delivery path, and an agent never receives its messages there.
Minimum Contract
Every session on Relay must be able to receive a message and be released.
type AgentSession = {
identity: AgentIdentity;
capabilities: AgentSessionCapabilities;
receiveMessage(message: RelayMessage, ctx: MessageContext): Promise<MessageReceipt>;
onEvent?(handler: (event: AgentSessionEvent) => void): Unsubscribe;
release?(reason?: string): Promise<void>;
};receiveMessage is the minimum hook that lets SDK code and harness adapters participate in delivery. A richer harness can also emit status, tool, transcript, file, terminal, and action events.
Delivery Modes
Delivery modes are semantic. They describe when the message should be surfaced to the session, not how a harness must implement it.
type DeliveryMode =
| 'immediate'
| 'next-message'
| 'next-tool-call'
| 'on-idle'
| 'manual';| Mode | Meaning |
|---|---|
immediate | Deliver now, even if that means interrupting or injecting into the active session boundary. |
next-message | Deliver when the harness is about to send the next user-level message to the session. |
next-tool-call | Deliver before the next tool-call boundary, useful for agents that should see coordination before taking another external action. |
on-idle | Deliver when the session reports idle, waiting, blocked, or another configured safe state. |
manual | Hold delivery until a caller or harness explicitly flushes pending work. |
The old question of "interrupt or not" is encapsulated by the delivery mode. immediate is the interrupting mode. The other modes are boundary-aware.
Delivery Context
Delivery context explains why the message exists and how the harness should treat it.
type MessageContext = {
id: string;
mode: DeliveryMode;
reason:
| 'message'
| 'mention'
| 'dm'
| 'thread-reply'
| 'action-result'
| 'notification';
priority?: 'normal' | 'urgent';
deadline?: Date | string;
idempotencyKey?: string;
metadata?: Record<string, unknown>;
};The context id is the delivery id. Harnesses should make duplicate delivery ids idempotent.
Receipts
The receipt is the harness telling Relay what happened.
type MessageReceipt =
| {
status: 'accepted';
deliveryId: string;
retryable?: boolean;
metadata?: Record<string, unknown>;
}
| {
status: 'delivered';
deliveryId: string;
metadata?: Record<string, unknown>;
}
| {
status: 'deferred';
deliveryId?: string;
availableAt: Date | string;
reason?: string;
metadata?: Record<string, unknown>;
}
| {
status: 'failed';
deliveryId?: string;
reason: string;
retryable?: boolean;
metadata?: Record<string, unknown>;
};Use accepted when the harness has queued or accepted responsibility for the message but has not yet surfaced it to the agent. Use delivered when the message was actually injected, displayed, or otherwise made available at the session boundary.
Use deferred when the mode is understood but the session cannot receive it yet. Use failed when the harness cannot or will not deliver it.
Delivery Runner
DeliveryRunner (@agent-relay/sdk/delivery) subscribes to an agent's inbox and drives delivery into a
target adapter or session. It is a class with two methods:
class DeliveryRunner {
constructor(options: {
messaging: RelayMessaging;
delivery: AgentDeliveryAdapter | AgentSession;
agentName?: string;
context?: DeliveryRunnerContext;
onResult?: (item: InboxItem, result: InjectionResult) => void | Promise<void>;
onError?: (item: InboxItem, error: unknown) => void | Promise<void>;
});
start(): Promise<void>;
stop(): void;
}start() requires messaging.capabilities.serverDeliveryState and throws RelayCapabilityError otherwise.
It consumes messaging.inbox.subscribe(...), calls the target for each item, then acks, defers, or
fails the inbox item based on the returned InjectionResult. A thrown error is failed with retry: true;
an adapter that returns status: 'failed' is failed with retry: false. stop() ends the loop.
Adapter Responsibilities
A delivery adapter owns the runtime-specific details.
interface AgentDeliveryAdapter {
readonly id: string;
readonly kind?: string;
readonly capabilities: DeliveryCapabilities;
connect?(): Promise<void>;
disconnect?(): Promise<void>;
inject?(message: RelayMessage, context: InjectionContext): Promise<InjectionResult>;
receiveMessage?(message: RelayMessage, context: MessageContext): Promise<MessageReceipt>;
getStatus?(): Promise<AgentRuntimeStatus>;
interrupt?(): Promise<void>;
onActivity?(handler: (event: AgentActivityEvent) => void): Unsubscribe;
}Examples:
- A Claude Code harness may inject text through a terminal-safe boundary and use hooks to detect idle or tool-call events.
- A Codex harness may use session-level notifications and tool-call boundaries when available.
- A custom app agent may call a callback directly inside its process.
All of those are valid as long as the adapter returns receipts and emits events that match the Agent Relay contract.
Event Flow
Delivery events are AgentSessionEvent variants a harness emits with relay.emitSessionEvent(...):
type DeliveryEvent =
| { type: 'delivery.accepted' | 'delivery.delivered'; messageId: string; deliveryId?: string }
| { type: 'delivery.deferred'; messageId: string; deliveryId?: string; availableAt: Date | string; reason?: string }
| { type: 'delivery.failed'; messageId: string; deliveryId?: string; reason: string; retryable?: boolean };Through addListener these arrive as { type, agentId, event } — the fields above live under event:
relay.addListener('delivery.failed', (e) =>
planner.sendMessage({
to: '#ops',
text: `Delivery ${e.event.deliveryId} failed for ${e.event.messageId}: ${e.event.reason}.`,
})
);WebSocket Role
WebSockets are how connected adapters hear about delivery work immediately. A harness adapter can subscribe to workspace events, filter deliveries for its session, and call receiveMessage.
Direct WebSocket clients are modeled as implicit direct_ws nodes. Broker-controlled WebSocket workers are
fleet_ws nodes that can host multiple bound agents and receive scoped context updates in addition to
durable delivery work.
If a WebSocket is not available, delivery can still work through polling, queue workers, app-server webhooks, HTTP push nodes, or MCP-triggered flush tools. The stored message and delivery record remain the source of truth.
HTTP Push Delivery
An http_push node stores an external delivery endpoint and one or more agent bindings. When Relay creates
a delivery for a bound agent, it dispatches the delivery to the node's URL with the configured auth mode.
ackMode controls acknowledgement:
manualleaves the delivery delivered until the receiver calls the ack endpoint with the bound agent token.on_2xxacks on any 2xx HTTP response.responseacks when the response body declares an ack.
Use on_2xx or response for webhook receivers that should not hold agent tokens. Queue-backed deployments
must run the HTTP push redrive sweep so due retries are dispatched after transient failures.
Reliability Rules
Harnesses and adapters should:
- Treat delivery ids as idempotency keys.
- Preserve event ordering per session.
- Redact secrets before emitting terminal, transcript, or tool events.
- Return a receipt for every delivery attempt.
- Use
deferredinstead of silent buffering when a delivery cannot happen yet. - Throw to request a retry.
DeliveryRunnerretries only on thrown errors; an adapter that returnsstatus: 'failed'is failed withretry: false. - Expose explicit unsupported errors for delivery-state operations the backend cannot persist yet.