Skip to main content

WebSocket API: GET /agents/connect

Persistent, agent-initiated WebSocket used by connected-mode agents to receive dispatches from the Svantic mesh. This is the only transport for connected-mode agents; see Agent Connectivity for the conceptual background. OpenAPI does not support WebSocket endpoints, so this reference lives as a Markdown page. The wire format is fully versioned under the subprotocol identifier svantic.v1 and is governed by the internal spec (platform/docs/specs/ws_transport.md, engineer-facing).

Endpoint

  • Scheme: wss:// only. ws:// is rejected.
  • Query parameters:
    • instance_id (required) — the instance that registered with deployment_mode: connected. Registration happens first over HTTPS; only after POST /agents/register returns a connect_url does the WebSocket upgrade succeed.
  • Subprotocol: client must offer svantic.v1 in Sec-WebSocket-Protocol. The server echoes it back in the 101 response. Clients that offer no compatible subprotocol are rejected with 400 UNSUPPORTED_SUBPROTOCOL.

Authentication

Upgrade requests carry a tenant-scoped JWT in the standard bearer header:
The same JWT used for POST /agents/register. Cookie authentication is not supported. The JWT’s tenant_id must match the tenant that owns the instance_id.

Handshake errors

The server validates four things, in order, before allocating a socket. Each failure returns a plain HTTP response — no 101 upgrade — with a JSON body under application/json. Example failure body:

Lifecycle

After 101 Switching Protocols the client must send a hello frame as its first text frame. The server replies with welcome; the connection is “live” only after the client observes welcome. The mesh will not push dispatch frames before the client reaches the ready state.

Frame envelope

Every frame is a UTF-8 JSON text frame. Binary frames are reserved for future use; if the server receives one it closes the socket with code 1003 Unsupported Data.
  • v — protocol version literal. Always 1 for svantic.v1. A breaking change ships under a new subprotocol identifier.
  • type — see the frame catalog below.
  • id — sender-assigned UUID v7. Used for correlation; response frames set in_reply_to to the request’s id.
  • ts — sender wall-clock in ISO 8601 / RFC 3339.
  • in_reply_to — required on response frames (dispatch_result, dispatch_chunk, dispatch_ack, tool_result, pong); otherwise omitted or null.
  • trace_id, parent_span_id — optional envelope-level W3C trace context for per-frame routing telemetry. Note that trace context for your business logic arrives inside the dispatch payload, at payload.session_context.propagation_headers (W3C traceparent + baggage).
  • payload — type-specific, documented per frame below.
Frames that fail schema validation receive an error frame (code: BAD_FRAME) and the server closes the socket with code 1002 Protocol Error.

Frame catalog

hello — agent → mesh

First frame after upgrade. Announces the agent and optionally asks to resume.

welcome — mesh → agent

Acknowledges hello. Transitions the client to ready.
When resumed: true, replayed_dispatches lists the dispatch.id values the server is re-delivering on this reconnect.

dispatch — mesh → agent

A single A2A task to execute. payload is byte-identical to the body the mesh would POST to a hosted agent for the same operation — so the same handler code works on both transports.

dispatch_ack — agent → mesh (optional)

Optional “received and working” signal. If the server does not receive a terminal response (result or error) before payload.deadline_ms, the dispatch times out regardless of whether an ack was sent.

dispatch_chunk — agent → mesh

A streaming output chunk. Multiple chunks, in order, may precede a dispatch_result. Receivers must preserve chunk ordering per in_reply_to group.

dispatch_result — agent → mesh

Terminal success for a dispatch.

tool_call / tool_result

When one agent invokes another agent’s tool, the request is routed through the mesh. tool_call has the same payload shape as a hosted tool invocation; tool_result is the reply.

heartbeat — agent → mesh

Self-reported presence, load, and health. Carries the same HeartbeatPayload as the HTTP POST /agents/heartbeat endpoint — hosted agents heartbeat over HTTP, connected agents heartbeat over this frame, but the payload bytes are identical and the gateway stores them in the same row.
Cadence: every 30 s (the shared HEARTBEAT_INTERVAL_MS constant), or immediately on any status change.

ping / pong

Application-level keepalive. Independent of the WebSocket protocol’s own ping/pong — both run in parallel to catch different failure modes.

error

Out-of-band error. Carries a stable machine-readable code the client can branch on; does not close the socket on its own unless the server decides it must.

close_request

Graceful shutdown request. The sender promises no new dispatch frames; in-flight dispatches continue up to payload.grace_seconds (default 30 s). After the grace window, the sender closes the socket with code 1000 Normal Closure.

Error frame catalog

payload.code values are stable. New codes may be added; existing codes never change meaning.

Heartbeats

Dead-peer timeout. Three missed app-level pings (~90 seconds) cause the server to close the socket with code 1001 Going Away. Clients should reconnect; the SDK handles this automatically.

Reconnect semantics

  • Bounded exponential backoff: 1 s, 2 s, 4 s, 8 s, 16 s, 30 s (cap), with ±25% random jitter applied to each step.
  • On every successful reconnect the backoff resets.
  • If the client carries a resume_token in hello, the server attempts to re-bind pending dispatches:
    • Matchwelcome.resumed = true, welcome.replayed_dispatches lists the re-delivered IDs.
    • No matchwelcome.resumed = false. Any dispatches that were in flight at the time of the disconnect have already failed on the original owner pod with AGENT_DISCONNECTED, and their callers have received the error.
  • Resume is pod-local. A reconnect that lands on a different pod (normal under scaling) cannot resume and starts clean.

Compatibility

  • v: 1 is fixed for the life of svantic.v1. Breaking changes ship as a new subprotocol (svantic.v2); both will be supported during the transition.
  • New optional fields may be added to existing payloads without a version bump. Receivers must ignore unknown fields.
  • New frame types may be added without a version bump. Receivers reply with error / BAD_FRAME but must not close the connection; the sender downgrades.

SDK support

If you’re writing your agent with @svantic/sdk, none of the above is your concern day-to-day — set deployment_mode: 'connected' at registration and the SDK dials, authenticates, reconnects, and resumes for you. Your capability handlers receive the same CapabilitySessionContext they would on a hosted deployment, with parent_trace_id, parent_span_id, and baggage already parsed off the incoming traceparent. This reference exists for:
  • Teams writing their own client (e.g. a non-TypeScript agent).
  • Debugging: reading ws logs from the SDK and matching them to protocol states.
  • Compliance reviews that need the wire format documented externally.