Delivery

Delivery is the contract that gets durable Relay messages into agent sessions and records what happened.

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';
ModeMeaning
immediateDeliver now, even if that means interrupting or injecting into the active session boundary.
next-messageDeliver when the harness is about to send the next user-level message to the session.
next-tool-callDeliver before the next tool-call boundary, useful for agents that should see coordination before taking another external action.
on-idleDeliver when the session reports idle, waiting, blocked, or another configured safe state.
manualHold 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:

delivery-listener.ts
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:

  • manual leaves the delivery delivered until the receiver calls the ack endpoint with the bound agent token.
  • on_2xx acks on any 2xx HTTP response.
  • response acks 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 deferred instead of silent buffering when a delivery cannot happen yet.
  • Throw to request a retry. DeliveryRunner retries only on thrown errors; an adapter that returns status: 'failed' is failed with retry: false.
  • Expose explicit unsupported errors for delivery-state operations the backend cannot persist yet.

Continue to the harness and session contract.