Skip to content

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
limitqueryintegerno
offsetqueryintegerno
org_idpathstring (uuid)yes

Responses

StatusDescriptionBody
200Successful ResponseChatListResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
org_idpathstring (uuid)yes

Request body (required)

FieldTypeRequiredDescription
environment_idstring (uuid)no
model_mode_overridestringnoPer-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_enabledbooleanno
titlestringno

Responses

StatusDescriptionBody
201Successful ResponseChatResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
chat_idpathstring (uuid)yes
org_idpathstring (uuid)yes

Responses

StatusDescriptionBody
200Successful ResponseChatResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
chat_idpathstring (uuid)yes
org_idpathstring (uuid)yes

Request body (required)

FieldTypeRequiredDescription
archivedbooleanno
sharedbooleanno
titlestringno

Responses

StatusDescriptionBody
200Successful ResponseChatResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
chat_idpathstring (uuid)yes
org_idpathstring (uuid)yes

Responses

StatusDescriptionBody
204Successful Response
422Validation ErrorHTTPValidationError

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 changes trigger_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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
chat_idpathstring (uuid)yes
org_idpathstring (uuid)yes

Responses

StatusDescriptionBody
200Successful ResponseChatResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
chat_idpathstring (uuid)yes
org_idpathstring (uuid)yes

Responses

StatusDescriptionBody
200Successful ResponseChatCostBreakdownResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
chat_idpathstring (uuid)yes
org_idpathstring (uuid)yes

Request body (required)

FieldTypeRequiredDescription
attachment_idsstring (uuid)[]no
contentstringyes
modelstringno
model_mode_overridestringnoPer-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_effortstringno

Responses

StatusDescriptionBody
202Successful ResponseSendMessageResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
chat_idpathstring (uuid)yes
org_idpathstring (uuid)yes

Responses

StatusDescriptionBody
200Successful ResponseChatPresenceResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
chat_idpathstring (uuid)yes
org_idpathstring (uuid)yes

Responses

StatusDescriptionBody
200Successful ResponseChatPresenceResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
chat_idpathstring (uuid)yes
org_idpathstring (uuid)yes

Responses

StatusDescriptionBody
201Successful ResponseChatResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
chat_idpathstring (uuid)yes
org_idpathstring (uuid)yes

Request body: StopChatRequest

Responses

StatusDescriptionBody
200Successful ResponseStopChatResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
chat_idpathstring (uuid)yes
org_idpathstring (uuid)yes

Responses

StatusDescriptionBody
200Successful ResponseTerminateChatResponse
422Validation ErrorHTTPValidationError

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:

  1. All agent_events for this chat_id — regardless of whether each row has assignment_id set or NULL. This captures agent_step prose rows written by a chat-lane turn before or without span attribution, which the per-assignment endpoint misses.

  2. Synthetic user_message_received events derived from agent_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.content carries the message text; metadata.attachments carries 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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
chat_idpathstring (uuid)yes
limitqueryintegernoMax events to return
offsetqueryintegernoPagination offset
org_idpathstring (uuid)yes

Responses

StatusDescriptionBody
200Successful ResponseConversationTimelineResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
chat_idpathstring (uuid)yes
org_idpathstring (uuid)yes

Responses

StatusDescriptionBody
200Successful ResponseTogglesResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
chat_idpathstring (uuid)yes
org_idpathstring (uuid)yes

Request body (required)

FieldTypeRequiredDescription
enabled_scopesstring[]yes

Responses

StatusDescriptionBody
200Successful ResponseTogglesResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
archivedquerybooleannoTrue returns archived chats only; false (default) returns active ones.
fromquerystring (date-time)noOnly chats whose last activity is at or after this.
limitqueryintegerno
offsetqueryintegerno
orderquery"asc" | "desc"no
org_idpathstring (uuid)yes
qquerystringnoCase-insensitive substring match on the chat title.
resolutionquerystring[]noRepeatable ChatResolution filter.
revisionqueryintegernoAgent revision version the chat runs on (pinned or inherited).
sortquery"activity" | "cost" | "messages"no
sourcequerystring[]noRepeatable ChatSource filter.
statusquerystring[]noRepeatable ChatStatus filter.
toquerystring (date-time)noOnly chats whose last activity is at or before this.

Responses

StatusDescriptionBody
200Successful ResponseChatBrowseResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
org_idpathstring (uuid)yes

Request body (required)

FieldTypeRequiredDescription
chat_idsstring (uuid)[]yes

Responses

StatusDescriptionBody
200Successful ResponseContinueChatsResponse
422Validation ErrorHTTPValidationError

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

NameInTypeRequiredDescription
agent_idpathstring (uuid)yes
archivedquerybooleannoWhen true, return archived conversations only; when false (default), return active (non-archived) conversations and chatless assignments.
cursorquerystringnoOpaque keyset cursor
limitqueryintegerno
org_idpathstring (uuid)yes

Responses

StatusDescriptionBody
200Successful ResponseUnifiedSidebarResponse
422Validation ErrorHTTPValidationError