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:
| Property | Task | Conversation |
|---|---|---|
single_shot | true — channel auto-closes after the first terminal reply | false — stays open across many turns |
| Deadline | deadlineMs from create, MS auto-closes when it expires (max 7 days) | None — natural Redis 24h-since-last-touch TTL; active SSE / POST /messages refreshes it |
| Status machine | pending → processing → completed / failed / cancelled (ADR-0017) | Plain state field: open or closed |
| Lifecycle terminator | First terminal reply auto-closes channel; POST .../cancel aborts mid-flight | DELETE /conversations/{id} closes it; otherwise lives until TTL |
POST /messages semantics | N/A — the initial message is the create body | Non-blocking 202; reply arrives via SSE / history |
| Webhook delivery | Yes — POST /tasks/{id}/webhooks (see webhooks guide) | No — observe via SSE / polling |
| SSE auto-close | Closes on terminal envelope (agent_reply, agent_reply_error, etc.) — event: end reason=task_terminal | Stays open until DELETE or client disconnect — event: end reason=channel_closed on delete |
| Caller turn budget | Each retry is a separate task (or POST /continue resumes a paused one) | Many turns per conversation; layered concurrency is allowed |
- 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).
| Method | Path | Purpose | Body | Returns |
|---|---|---|---|---|
POST | /api/v1/agents/{agentId}/conversations | Create a new dialog channel | { title?, metadata?: {...} } | 201 + Conversation |
GET | /api/v1/agents/{agentId}/conversations | List caller’s conversations with this agent | — | 200 + { conversations, next_since } |
GET | /api/v1/agents/{agentId}/conversations/{convId} | Get one conversation | — | 200 + Conversation |
DELETE | /api/v1/agents/{agentId}/conversations/{convId} | Close the conversation | — | 204 |
POST | /api/v1/agents/{agentId}/conversations/{convId}/messages | Send a user turn (non-blocking) | { message, idempotency_key? } | 202 + { message_id, created_at } |
GET | /api/v1/agents/{agentId}/conversations/{convId}/messages | Paginated history | ?since=<offset>&limit=<n> | 200 + { messages, latest_offset } |
GET | /api/v1/agents/{agentId}/conversations/{convId}/events | SSE live stream | ?since=<offset> | SSE stream |
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:| HTTP | code | Cause |
|---|---|---|
400 | invalid_param | convId empty or conv_id belongs to a different agent than the URL path |
400 | invalid_param | conv_id refers to a non-conversation channel — you tried to use a task ID on a conversation endpoint |
403 | forbidden | The conversation isn’t yours |
404 | agent_not_found | Conversation doesn’t exist or fell off the MS TTL |
409 | conflict | Channel is closed (e.g. POST /messages after DELETE) |
413 | payload_too_large | Message body > 1 MiB — split into multiple turns or use attachments for large blobs |
503 | agent_unavailable | MESSAGE_SERVICE_URL not configured, MS dead, or agent is offline (POST /messages only — list / get / SSE will still work against the historical log) |
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)