Skip to main content
Audience: SDK users building chat UIs, multi-turn assistants, or any workflow that needs to send more than one message to the same agent without re-establishing context.
If you only need a single request/reply or a single long job with a deadline, you want Calling Agents instead.

1. Which one do I want?

Both tasks and conversations ride the same underlying message channel in Message Service — the difference is purely a gateway-level UX choice: Rule of thumb:
  • Chat UI → conversation
  • Cron job / background process / batch report → task
  • One-off Q&A → blocking invoke (simplest)

2. Lifecycle (conversation)

Note: Conversations don’t have a chat_cancel envelope. There’s no single in-flight turn to abort — if you want to stop the agent from continuing the latest turn, DELETE the entire conversation (which closes the channel). The agent receives a normal channel close signal and stops streaming.

3. The five conversation endpoints

All five require Authorization: Bearer <JWT or oag_...>. oag_ keys are user-scoped: any key whose owner owns the underlying conversation can call every endpoint here (see Authentication & API Keys for the authorization model — per-route scope gating was removed in v1.1.0). Authorization invariant: caller can only see / mutate their own conversations. The conversation’s metadata.caller_owner_id is written at create time; every read / write call checks it. A 403 forbidden ("conversation is not owned by caller") is returned on ownership mismatch.

4. The since cursor (history + SSE)

since is an integer offset on the conversation’s monotonic message log. The wire treats it as opaque, but here’s the model so you can reason about it:
  • since=0 (or omitted) — start from the beginning. Useful for late-attaching SSE clients that want the full transcript.
  • since=Nresume from offset N+1. Pass the last offset you successfully observed.

GET /messages

Use latest_offset from the previous page as the next since. Page size: default 200, max 500 (Message Service limit).

GET /events (SSE)

The stream replays every message at offset > 4, then keeps the connection open for new ones. Each frame:
  • offset is per-frame: track the latest one and pass it as since=<latest> on reconnect. SSE clients that crash and reconnect must NOT pass since=0 — that re-replays the full history and duplicates every event you already processed.
  • event: end (with reason) is emitted once before the connection terminates. Possible reasons:
    • channel_closedDELETE was called (by you or a peer with the same auth) on this conversation.
    • stream_closed — Message Service closed the upstream stream (rare; usually a server restart or the conversation TTL fired).
No equivalent of the task SSE’s task_terminal end reason exists for conversations — there’s no terminal frame in the open-ended model. The connection only ends on close / disconnect.

Keepalive

The connection produces no traffic during idle periods. Browsers and some HTTP proxies will close idle connections after ~60s. Mitigation:
  1. Run your client behind a proxy that doesn’t kill idle SSE (most CDNs handle SSE natively); or
  2. Reconnect on socket close, passing the latest observed offset as since=<offset> so you don’t miss any events.

5. Worked example — chat loop (Node.js fetch + EventSource)


6. Concurrency, idempotency, attachments

Multiple in-flight turns

The conversation API explicitly allows queuing multiple turns before the agent has replied to the previous one. Replies arrive in the order the agent produces them; the in_reply_to field on each agent_reply points back to the originating user message_id, so clients can correlate even when turns interleave.

Idempotency

Pass an idempotency_key on POST /messages. Message Service deduplicates on (channel_id, idempotency_key). Resending the same key on a network retry returns the existing message_id without re-publishing — safe to wire into automatic retries. If omitted, the gateway generates a fresh UUID, so retries from different fetch attempts will be treated as distinct messages.

Attachments

Conversations accept attachments the same way invoke and tasks do — upload via POST /api/v1/files/presign-upload, then include the returned file_id in the next POST /messages body:
The gateway resolves each file_id to a presigned download URL and embeds it in the chat envelope so the agent can fetch the bytes out-of-band. See Calling Agents § Attachments for the full flow.

7. Deletion semantics

What happens:
  1. The channel is closed with reason=canceled.
  2. Any SSE connection on /events receives one final event: end\ndata: {"reason":"channel_closed"}\n\n and the stream terminates.
  3. The conversation is now state=closed — subsequent POST /messages calls return 409 conflict ("channel closed").
  4. History remains readable via GET /messages for the MS 5-minute grace window post-close; after that the channel is evicted and reads return 404 agent_not_found ("conversation not found").
If you want to keep the transcript indefinitely, fetch all messages before calling DELETE — the gateway is not a long-term archive, only Message Service’s TTL-bounded log.

8. Errors

The full set lives in Error Reference. The ones you’re most likely to hit on this API:

9. See also