POST /v1/conversations/:id/messages
Auth: tenant — _Status:_ stable
Send a user message and stream the assistant's reply. Same SSE response shape as POST /v1/orchestrate, plus a conversation_id field on the done event. Billed.
Request
| Path param | Type | Description |
|---|---|---|
id | int | Conversation id. |
Body
| Field | Type | Required | Description |
|---|---|---|---|
message | string | yes | The new user turn. |
original_query | string | no | Same as POST /v1/orchestrate — pins the advisor gate's original task on continuation turns. |
agent_def | object | no | Override the conversation's snapshotted agent for this one turn (rare — usually a follow-up should just send message). When omitted, the conversation's snapshot from create time is reused. |
curl -N \
-H "Authorization: Bearer atr_…" \
-H "Content-Type: application/json" \
-d '{"message":"and what about cache writes?"}' \
http://arbiter.example.com/v1/conversations/1/messages
Headers
Idempotency-Key is honoured here on the same terms as on POST /v1/orchestrate — retries replay the original execution instead of triggering a duplicate, the assistant turn isn't persisted twice, and the dedup map is durable in tenants.db with a 24h TTL.
What happens server-side
- Conversation lookup —
404if missing or wrong tenant. Validation surfaces as a clean JSON error before the SSE stream opens. - The conversation's snapshotted
agent_defis applied to the orchestrator (unless the request body supplied its own, which wins). This is what makes follow-ups work without re-sending the agent definition. - Prior messages loaded from the DB and replayed into the agent's history (capped at the newest 100 turns to keep request payload bounded). Any rolling context-compaction summary is restored by locating the saved boundary (prefer durable
boundary_db_id, else a unique content match) in that hydrated tail. If the boundary has fallen off the cap, messages between the boundary and the replay window are folded into the rolling summary before the turn proceeds. - The user's
messageis persisted with therequest_idissued for this stream. - The orchestrator runs and streams events exactly as
POST /v1/orchestratewould. - On a successful
done, the assistant's full cumulative response (across every tool-call re-entry iteration) is persisted alongside the request'sinput_tokens/output_tokenstotals, and the conversation's compaction summary is updated when compaction ran. - On failure (
done.ok = false), the assistant message is not persisted — only the user message remains. Retry is safe.
Response
Content-Type: text/event-stream. Identical to POST /v1/orchestrate, plus conversation_id on the terminal done event.
The request_id from this call's request_received event is the handle to pass to POST /v1/requests/:id/cancel if the user clicks Stop.
Failure modes
| Status | When | Body |
|---|---|---|
| 400 | Body isn't a JSON object; missing message; agent_def shape invalid. | {"error": "..."} |
| 401 | Missing / invalid bearer. | {"error": "..."} |
| 404 | Conversation doesn't exist or belongs to another tenant. | {"error": "conversation not found"} |
200 + done.ok = false | LLM upstream failure, billing-service denial, transient I/O. See POST /v1/orchestrate. | SSE stream |
See also
POST /v1/orchestrate— single-turn variant without history replay.GET /v1/conversations/:id/messages— read history back.POST /v1/requests/:id/cancel, SSE event catalog.