Agent Chats
GET /api/orgs/{org_id}/agents/{agent_id}/chats/
Section titled “GET /api/orgs/{org_id}/agents/{agent_id}/chats/”List Chats
List chat conversations for an agent (no message content — summary only).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| limit | query | integer | no | |
| offset | query | integer | no | |
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ChatListResponse |
| 422 | Validation Error | HTTPValidationError |
POST /api/orgs/{org_id}/agents/{agent_id}/chats/
Section titled “POST /api/orgs/{org_id}/agents/{agent_id}/chats/”Create Chat
Create a new chat conversation with an agent.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| org_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| environment_id | string (uuid) | no | |
| model_mode_override | string | no | Per-chat effort level (‘trivial’ | ‘normal’ | ‘high_effort’), beating the agent’s own model_mode for this chat only. Only meaningful for an org whose plan selects effort rather than models: setting it on an org with direct model choice is a 422, because the value would be silently inert. Null inherits the agent’s level. See GET /api/orgs/{org_id}/model-modes for the available levels, their labels and their credit burn ratios. |
| sandbox_enabled | boolean | no | |
| title | string | no |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | Successful Response | ChatResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}
Section titled “GET /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}”Get Chat
Get a chat conversation with all messages.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| chat_id | path | string (uuid) | yes | |
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ChatResponse |
| 422 | Validation Error | HTTPValidationError |
PATCH /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}
Section titled “PATCH /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}”Update Chat
Update a chat — title, archived state, and/or shared visibility.
At least one of title, archived or shared must be provided.
shared (private↔shared visibility) is creator-only.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| chat_id | path | string (uuid) | yes | |
| org_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| archived | boolean | no | |
| shared | boolean | no | |
| title | string | no |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ChatResponse |
| 422 | Validation Error | HTTPValidationError |
DELETE /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}
Section titled “DELETE /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}”Delete Chat
Soft-delete a chat conversation.
Deleting closes the chat as surely as /terminate does — more so, since
the row stops being reachable at all — so it resolves any pending HITL
request to cancelled and invalidates unfinished todos the same way. A
request left pending here can never be answered (its chat 404s) and never be
cleared, so it sits in the HITL inbox forever.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| chat_id | path | string (uuid) | yes | |
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 204 | Successful Response | |
| 422 | Validation Error | HTTPValidationError |
POST /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/continue
Section titled “POST /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/continue”Continue Chat
Carry this chat on — same chat, same transcript, next turn.
Two shapes of stuck chat land here, and the difference is what the button says, never what this endpoint does:
- Continue — a liftable blocker stopped it: the org ran out of credits,
or the model it runs on had no price to charge credits against
(
CONTINUABLE_STOP_REASONS). Both ARCHIVE the chat, so it sits there with everything the agent had already done and no way to reach it. - Run again — the last turn simply failed (
ERRORED: a provider that refused, a stream that died). Nothing archived it and its composer still works, so until this endpoint accepted it the only way to carry the chat on was to type a message like “continue” — inventing a user turn that says nothing, purely to trigger the dispatch. That message is also not free: it changestrigger_message_id, which keys the checkpointer thread, so the failed turn’s committed state is abandoned and its tool work re-done. Dispatching with no new message resumes it instead.
/retry is neither: a retry is a clone that starts the work over,
discarding the transcript this endpoint exists to keep.
Deliberately a button rather than something a top-up does by itself. “Money arrived” is not “and I still want all forty of those turns to run”: some of the blocked work will have been overtaken by events, and re-running it costs real credits. So the person who paid picks the chats that still matter.
The blocker is re-checked here, not trusted from the client. A tab that has been open since before the balance emptied still shows the button, and dispatching on its say-so would burn a worker slot to stop the chat again two seconds later — with a second identical notice in the transcript. An org that is still out of credits gets the same 402 every other ingress does.
A model that is still unpriced cannot be re-checked as cheaply (it needs the chat’s resolved model and route, which only the runtime knows), so that one is allowed through: the turn refuses at the gate exactly as before and the chat lands back where it was, one wasted admission later.
Un-archiving is done here and nowhere else. dispatch_conversation_turn
revives PAUSED and ERRORED chats — the states a fresh user message revives —
and ARCHIVED is deliberately not in that set: it is the terminal a user
reaches with /terminate, and a message must never quietly reopen it.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| chat_id | path | string (uuid) | yes | |
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ChatResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/cost-breakdown
Section titled “GET /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/cost-breakdown”Get Chat Cost Breakdown
Per-token-type cost attribution for this chat’s LLM spend.
Split out from GET /{chat_id} rather than inlined on ChatResponse:
the popover that shows this is opened on demand, and every other chat
surface (list, sidebar, detail) has no use for a per-line breakdown on
every row. See chat_cost_breakdown for the attribution rules — in
short, totals are always the stored cost_usd (never recomputed from
rates), and a credits org gets the same shape with every dollar field
null, same as everywhere else on this API (docs/api/spend-display.md).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| chat_id | path | string (uuid) | yes | |
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ChatCostBreakdownResponse |
| 422 | Validation Error | HTTPValidationError |
POST /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/messages
Section titled “POST /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/messages”Send Message
Send a user message and dispatch the agent turn to the worker.
Persists the user message immediately (for optimistic echo + sidebar bump),
then dispatches a CONVERSATION_TURN_REQUESTED event to the worker.
The worker owns LLM execution, assistant-message persistence, cost rollup,
and title generation.
Returns 202 Accepted with the persisted user message id. Returns 503 if the worker dispatch fails (a silent 202 would leave the turn never running).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| chat_id | path | string (uuid) | yes | |
| org_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| attachment_ids | string (uuid)[] | no | |
| content | string | yes | |
| model | string | no | |
| model_mode_override | string | no | Per-chat effort level (‘trivial’ | ‘normal’ | ‘high_effort’), beating the agent’s own model_mode for this chat only. Only meaningful for an org whose plan selects effort rather than models: setting it on an org with direct model choice is a 422, because the value would be silently inert. Null inherits the agent’s level. See GET /api/orgs/{org_id}/model-modes for the available levels, their labels and their credit burn ratios. |
| reasoning_effort | string | no |
Responses
| Status | Description | Body |
|---|---|---|
| 202 | Successful Response | SendMessageResponse |
| 422 | Validation Error | HTTPValidationError |
POST /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/presence
Section titled “POST /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/presence”Heartbeat Presence
Say “I am looking at this chat” and get back everyone else who is.
Called on an interval by an open chat tab. The roster is live Redis state: a viewer who stops calling ages out on its own, so a crashed browser or a closed laptop cannot leave a ghost in the header.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| chat_id | path | string (uuid) | yes | |
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ChatPresenceResponse |
| 422 | Validation Error | HTTPValidationError |
DELETE /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/presence
Section titled “DELETE /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/presence”Leave Presence
Leave the roster immediately, rather than waiting to age out.
Best effort — the stale cutoff is what actually guarantees the roster stays honest, since a closing tab is not a reliable narrator.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| chat_id | path | string (uuid) | yes | |
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ChatPresenceResponse |
| 422 | Validation Error | HTTPValidationError |
POST /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/retry
Section titled “POST /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/retry”Retry Chat
Re-run this chat’s inputs from scratch, in a brand new chat.
A retry is a clone, never a resume: the original is left exactly as it is (so a failed run stays readable) and a fresh chat is created carrying the same opening user turns, the same attachments, the same originator context and the same per-chat runtime choices — then dispatched immediately.
The agent’s own configuration (instruction, tools, skills, team context) is re-frozen from its CURRENT state rather than copied from the source chat’s snapshots: fixing the agent and pressing retry is the point of the button.
Returns 201 with the new chat, whose id the caller opens. 503 if the worker dispatch fails — the clone exists but would never run, so it must not read as success.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| chat_id | path | string (uuid) | yes | |
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 201 | Successful Response | ChatResponse |
| 422 | Validation Error | HTTPValidationError |
POST /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/stop
Section titled “POST /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/stop”Stop Chat
Stop this chat’s work — keep the chat itself alive.
A RUNNING chat flips to ACTIVE so the in-flight worker aborts at its next
LLM step (pre_llm_checks raises AssignmentCancelledError whenever
chat.status != RUNNING), and stop_requested_at is stamped so a turn
that was dispatched but had not yet started is skipped at entry instead of
flipping itself back to RUNNING.
A chat that is merely waiting is stopped too, and this is the case that
matters most: a parent parked on the children it spawned
(awaiting_children), a chat asleep on its own timer, or one queued behind
the org’s concurrency cap is not running, but it is going to start again by
itself. Its pause is cleared and the machinery that would resume it — the
open child-chat wait, any pending timer — is torn down. A chat paused on a
human keeps its pause: stopping the work must not discard the question
somebody was asked.
With cascade the same treatment is applied to every chat descended from
this one, to any depth and across agents — an agent that fanned work out to
children otherwise leaves them all running (and spending) when the chat the
user is watching stops. Each stopped chat’s open child-chat wait is
abandoned in the same transaction, so a child settling a moment later cannot
wake the subtree back up.
Unlike /terminate the chat is not closed: no completed_at,
no archived_at, and no HITL resolution. The chat stays live and the
user can immediately send another message.
Idempotent: if the chat is not RUNNING (already ACTIVE, PAUSED, or in a terminal state) 200 is returned without mutation — stopping a non-running chat is always a no-op.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| chat_id | path | string (uuid) | yes | |
| org_id | path | string (uuid) | yes |
Request body: StopChatRequest
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | StopChatResponse |
| 422 | Validation Error | HTTPValidationError |
POST /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/terminate
Section titled “POST /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/terminate”Terminate Chat
Terminate a chat — set status to ARCHIVED and resolve any pending HITL.
Flips chat.status to ARCHIVED (the user-closed terminal state) and
sets completed_at to now. The ORM listener fires a status_changed
realtime envelope so the frontend receives the update immediately.
Any pending HITL request for this chat is resolved to cancelled so the
chat is not left awaiting human input.
Idempotent: if the chat is already in a terminal status (ERRORED, ARCHIVED, ABANDONED) 200 is returned without re-mutating.
Setting status away from RUNNING signals an in-flight runner to abort at its
next LLM step (pre_llm_checks raises AssignmentCancelledError). No
extra worker call is needed.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| chat_id | path | string (uuid) | yes | |
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | TerminateChatResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/timeline
Section titled “GET /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/timeline”Get Conversation Timeline
Get the unified conversation-spine timeline for a chat.
Returns a newest-first list of events composed from:
-
All
agent_eventsfor thischat_id— regardless of whether each row hasassignment_idset or NULL. This capturesagent_stepprose rows written by a chat-lane turn before or without span attribution, which the per-assignment endpoint misses. -
Synthetic
user_message_receivedevents derived fromagent_chat_messages WHERE role='user'for this chat. User messages are not on the event spine, but the frontend renders the whole conversation from this single feed, so user turns are synthesised here.metadata.contentcarries the message text;metadata.attachmentscarries any file attachments so the frontend can render file chips.
The merged list is sorted newest-first (same convention as
GET /assignments/{id}/timeline) so the frontend buildTurns
(which does [...events].reverse()) works unchanged.
Pagination: limit / offset are applied AFTER the merge-sort, so
the total reflects the combined event + user-message count.
Requires membership in the organization and that the chat belongs to the given agent.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| chat_id | path | string (uuid) | yes | |
| limit | query | integer | no | Max events to return |
| offset | query | integer | no | Pagination offset |
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ConversationTimelineResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/toggles
Section titled “GET /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/toggles”Get Chat Toggles
Get the available composer toggles for a chat with their current enabled state.
Always includes a system:web entry for the web-search bundle. Also
includes one entry per ACTIVE Composio integration connected to this org.
The enabled field reflects whether the scope is currently active for
the chat (i.e. is present in enabled_scopes).
Only connections this agent can reach are offered — see
:func:_build_toggle_items. Only Composio integrations are surfaced here —
MCP-server and other integration types are not toggle-able via this surface.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| chat_id | path | string (uuid) | yes | |
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | TogglesResponse |
| 422 | Validation Error | HTTPValidationError |
PUT /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/toggles
Section titled “PUT /api/orgs/{org_id}/agents/{agent_id}/chats/{chat_id}/toggles”Update Chat Toggles
Update the composer toggle state for a chat.
Replaces enabled_scopes with the provided list. Every scope in the
request body must be one of the allowed values for this chat (system:web
or a composio:<toolkit> scope for an ACTIVE Composio integration this
agent can reach) — anything else is rejected with 422. Enabling a scope
the agent cannot resolve is refused rather than stored: unlike an agent’s
allowed_tools, a chat toggle names a connection that exists now and is
acted on this turn, so there is no “granted before it existed” ordering to
protect and a stored one could only ever be a lie.
Returns the updated toggle list (same shape as GET /{chat_id}/toggles).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| chat_id | path | string (uuid) | yes | |
| org_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| enabled_scopes | string[] | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | TogglesResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/orgs/{org_id}/agents/{agent_id}/chats/browse
Section titled “GET /api/orgs/{org_id}/agents/{agent_id}/chats/browse”Browse Chats
The All chats table: the sidebar’s rows, filtered, sorted and paged.
Deliberately separate from /unified in signature and identical in row
shape. The sidebar is a keyset feed of recent activity and must stay that
way (it appends as you scroll); a table needs page numbers, a total, and
sorts that reorder the whole set, which keyset cannot express. Both read
:func:_chat_list_select, so the two can’t disagree about a row.
status and resolution are separate filters because they answer
different questions: status is “is this chat moving?”, resolution is “how
did the work come out?”. Folding them into one filter is what made
“finished, but a human still has to fix it” unfindable.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| archived | query | boolean | no | True returns archived chats only; false (default) returns active ones. |
| from | query | string (date-time) | no | Only chats whose last activity is at or after this. |
| limit | query | integer | no | |
| offset | query | integer | no | |
| order | query | "asc" | "desc" | no | |
| org_id | path | string (uuid) | yes | |
| q | query | string | no | Case-insensitive substring match on the chat title. |
| resolution | query | string[] | no | Repeatable ChatResolution filter. |
| revision | query | integer | no | Agent revision version the chat runs on (pinned or inherited). |
| sort | query | "activity" | "cost" | "messages" | no | |
| source | query | string[] | no | Repeatable ChatSource filter. |
| status | query | string[] | no | Repeatable ChatStatus filter. |
| to | query | string (date-time) | no | Only chats whose last activity is at or before this. |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ChatBrowseResponse |
| 422 | Validation Error | HTTPValidationError |
POST /api/orgs/{org_id}/agents/{agent_id}/chats/continue-batch
Section titled “POST /api/orgs/{org_id}/agents/{agent_id}/chats/continue-batch”Continue Chats
Continue a selection of chats — the All chats table’s bulk action.
One provider outage or one exhausted balance strands chats by the dozen, all at once and all for the same reason. Clearing that by opening each chat and pressing the same button is the actual cost of the incident, and it is why the table exists.
Partial success is the contract, not a fallback. A selection is a rough gesture over a filtered table: some rows will have moved on, been archived, or already been continued from another tab. Failing the whole call because one of thirty is no longer continuable would make the feature unusable for exactly the mess it is for. Every chat is attempted and reported on individually, in the order it was asked for, and each one that started is genuinely dispatched — this returns no optimistic rows.
Sequential on purpose. Each chat commits its own revival before the next is touched, so an error partway leaves the ones before it running rather than rolling them back, and the credit gate is re-read per chat: a batch that empties the balance halfway must stop spending, not discover it at the thirtieth worker.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| org_id | path | string (uuid) | yes |
Request body (required)
| Field | Type | Required | Description |
|---|---|---|---|
| chat_ids | string (uuid)[] | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | ContinueChatsResponse |
| 422 | Validation Error | HTTPValidationError |
GET /api/orgs/{org_id}/agents/{agent_id}/chats/unified
Section titled “GET /api/orgs/{org_id}/agents/{agent_id}/chats/unified”List Unified Sidebar
Unified sidebar list: conversations ordered by activity.
Phase 2 (chat-outcomes model): chatless assignment items are gone — every work unit is a chat.
Returns a keyset-paginated list of conversations for the agent sidebar.
When archived=true, only archived conversations are returned.
The response includes archived_count=0 on this branch.
When archived=false (default), only active (non-archived) conversations
are returned. The response includes archived_count with the total number
of archived conversations for this agent.
Keyset pagination: cursor encodes (activity_at ISO, id) as base64. Use next_cursor from the response to fetch the next page.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| agent_id | path | string (uuid) | yes | |
| archived | query | boolean | no | When true, return archived conversations only; when false (default), return active (non-archived) conversations and chatless assignments. |
| cursor | query | string | no | Opaque keyset cursor |
| limit | query | integer | no | |
| org_id | path | string (uuid) | yes |
Responses
| Status | Description | Body |
|---|---|---|
| 200 | Successful Response | UnifiedSidebarResponse |
| 422 | Validation Error | HTTPValidationError |