Skip to main content
The Trellis chat API turns natural-language questions into answers over your data. Create a chat first, then submit each answer as a durable turn over either the per-agent WebSocket or the detached REST turn API.

Choose a turn transport

Both transports create the same persisted messages and use the same durable turn machinery. A socket disconnect detaches the client; it does not cancel the turn.

Lifecycle

  1. Create a chat. Save both id and agent_id.
  2. Optionally upload files and save their IDs.
  3. Start a turn over the WebSocket or with POST /v1/agents/{agent_id}/chats/{chat_id}/turns.
  4. Keep the returned turn_id. Only one turn may be in_progress for a chat at a time.
  5. Continue with the same chat_id, or load persisted history with Get Chat.
There is no lazy chat creation. A WebSocket send_message without chat_id returns MISSING_CHAT_ID; an unknown or unowned chat returns CHAT_NOT_FOUND.

WebSocket turn sequence

Every accepted WebSocket send or edit first returns turn_accepted at seq: 0. Durable replayable frames then begin at sequence 1.
Partition multiplexed traffic by turn_id, order and deduplicate it by (turn_id, seq), and persist the greatest sequence seen for each turn.

Events

A turn ends with exactly one enveloped message, error, or interrupted event. The bare interrupted control frame is only an interrupt acknowledgement and is not terminal. This table describes turn-enveloped events. WebSocket errors sent during socket setup/control, before turn_accepted, and after turn acceptance use three different contexts, and code is not universal across them. See WebSocket error frames.

Streaming text

Each text_delta.data.content is a new chunk, not a running total. On terminal message, replace the assembled buffer with message.data.content; it is the authoritative persisted answer. If a turn is cancelled, the enveloped terminal interrupted.data.content is the authoritative persisted partial answer. If a turn fails, refetch the chat: partial content can be represented by the durable turn snapshot while the error is recorded.

Durable recovery

On a cold load, treat running_turn_id, not is_processing, as the authoritative signal that the chat still owns a running turn. is_processing is a liveness hint and can be false while a stale running row is awaiting server resolution. To recover a disconnected WebSocket turn:
  1. Call GET /v1/chats/{chat_id} and replace local history with messages; render active_turn.partial_content as the in-progress assistant text.
  2. Advance the cursor to running_turn_last_seq. That watermark covers both the returned messages and active_turn.partial_content; do not append an older streaming buffer to the snapshot.
  3. While running_turn_id still equals the turn, keep the composer locked, reconnect, and send resume from the refreshed watermark.
  4. If exact replay frames arrive, deduplicate by (turn_id, seq) and continue. Exact replay is a latency optimization, not the correctness boundary.
  5. If an enveloped resume fallback frame arrives, that subscription will send no more turn frames. Immediately repeat the REST reconciliation and retry after backoff.
  6. When running_turn_id becomes null, the terminal persisted state is in messages and the composer can unlock.
The resume.data.status values are raw lifecycle values (running, done, error, cancelled). REST turn status uses the public values in_progress, completed, failed, and cancelled. Branch on alive and status; treat reason (gap, degraded, or expired) as diagnostic context.

Legacy chats without an agent binding

New chats always return a non-null agent_id. Older chats can return agent_id: null from List Chats and Get Chat. Keep your organization’s configured published agent UUID in client configuration, verify it against List Agents, and use that ID in the WebSocket or REST-turn URL. The current display name and is_trellis_default flag are historical metadata; drive the fallback by the configured ID, not by either label. The server accepts that agent for an unbound legacy row, but does not backfill the row. Apply the fallback every time the chat is loaded.

Shared limits

Limits are keyed by the authenticated member_id, not by JWT or socket. Re-authenticating does not reset them, and multiple JWTs for the same member share the same buckets. HTTP throttles return 429 with Retry-After. A throttled WebSocket send/edit receives an enveloped pre-turn error with code: "RATE_LIMIT" and retry_after_seconds.

Persisted message roles and types

Persisted messages contain role, type, content, timestamp, and metadata. Tool call/result content is a JSON-encoded string in REST history. Parse it before dispatching on its structured fields.