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?
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 requireAuthorization: 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=N— resume from offsetN+1. Pass the last offset you successfully observed.
GET /messages
latest_offset from the previous page as the next since. Page
size: default 200, max 500 (Message Service limit).
GET /events (SSE)
> 4, then keeps the
connection open for new ones. Each frame:
offsetis per-frame: track the latest one and pass it assince=<latest>on reconnect. SSE clients that crash and reconnect must NOT passsince=0— that re-replays the full history and duplicates every event you already processed.event: end(withreason) is emitted once before the connection terminates. Possible reasons:channel_closed—DELETEwas 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:- Run your client behind a proxy that doesn’t kill idle SSE (most CDNs handle SSE natively); or
- Reconnect on socket close, passing the latest observed
offsetassince=<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; thein_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 anidempotency_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 wayinvoke and tasks
do — upload via POST /api/v1/files/presign-upload, then include
the returned file_id in the next POST /messages body:
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
- The channel is closed with
reason=canceled. - Any SSE connection on
/eventsreceives one finalevent: end\ndata: {"reason":"channel_closed"}\n\nand the stream terminates. - The conversation is now
state=closed— subsequentPOST /messagescalls return409 conflict("channel closed"). - History remains readable via
GET /messagesfor the MS 5-minute grace window post-close; after that the channel is evicted and reads return404 agent_not_found("conversation not found").
8. Errors
The full set lives in Error Reference. The ones you’re most likely to hit on this API:9. See also
- Calling Agents — the three single-turn invocation modes
- Streaming (SSE) — full reference for the
/eventsendpoint’s wire framing,sincesemantics, and reconnect / keepalive recipes - Webhooks — push notifications for the task API (no equivalent for conversations)
- Error Reference — every
codethe conversation endpoints can emit - Authentication & API Keys — credential format and the owner-ACL authorization model
- OpenAPI contract:
backend/openapi/beeos-platform-v1.yaml(search forconversations:inpaths)