assistant profile of the same agent core that powers the Composer. The two are separated only by kind and profile: the Composer edits a workflow graph, while the Assistant has no workflow tools at all and acts directly on your connected integration tools and knowledge bases.
The reason, act, observe loop
A turn is an agentic loop. The Assistant alternates between calling the language model (reason) and calling exactly one tool (act), then feeds the tool result back into the model (observe), and repeats until the model produces a final answer with no further tool call. Each pass of the loop produces streamed events you can observe live over SSE:Reason — the model decides the next step
response_chunk events. It either ends the turn with a final answer, or it decides to call a tool.Act — one tool runs
tool_call event and runs that single action. Tool calls are serialized: SerialToolCallsMiddleware forces one tool call per model step, so two tools never fire in the same step. The Assistant can call a tool that discovers integrations, runs an integration action, searches knowledge, or asks you a human-in-the-loop question.Observe — the result feeds back
tool_result event carrying the tool’s output (or an error payload on failure) and feeds it back to the model. The loop returns to the reason step.Repeat until the model is done
done event with the final response and usage totals, and the turn ends.get_available_integrations, get_integration_details, get_organization_credentials), execution (execute_integration_tool), knowledge (search_knowledge), and the HITL tools (ask_user_choice, ask_user_multi_choice, ask_user_yes_no, ask_user_free_text, request_credential). Workflow inspect, build, edit, and run tools are deliberately excluded — a resumed Assistant run can never regain them because the profile is re-derived from the chat’s kind.
Storage and execution model
Assistant and Composer chats share the same conversation store, distinguished by akind value (assistant vs composer), so they cannot read or operate on each other’s chats. Every service call is scoped by kind. The Assistant intentionally reuses the Composer’s agent factory, executor, HITL interrupt/resume machinery, OAuth auto-resume, and SSE streaming — staying in the shared store is required so that OAuth auto-resume keeps working.
SerialToolCallsMiddleware and SummarizationMiddleware plus provider prompt caching, and skips the todo-list, filesystem, and subagent middleware), and uses an Assistant system prompt with no workflow or focus language. The save, revert, and focus endpoints that the Composer exposes are omitted entirely.
Turns and runs
These two words are not interchangeable; the distinction matters for billing, streaming, and identity.Turn
POST /assistant/chat and ends when the loop reaches a final answer, pauses for input, is cancelled, or errors. Billing charges exactly one run credit per turn.Run
run_id. A turn maps to one run — but when a turn pauses for human-in-the-loop input and you resume it, a new run_id is minted for the continuation. So one logical turn can span more than one run_id.The three run-id identities
run_id is used at several layers, and you must not assume one identity. The same caution applies to workflow runs — see workflows and runs for the full treatment.
run_id is minted on every resume, so it is not stable across a multi-step conversation and is not a conversation-level key.thread_id == chat_id and is stable across the whole conversation. Every run in a chat re-enters the same thread.How a turn is admitted, charged, and tracked
WhenPOST /assistant/chat is called, the Assistant runs a synchronous, reject-before-write sequence so a denied turn never writes any rows:
Concurrency guards
Billing admission gate
{chat.id}:{run_id}, usage type run, and rate class sync_exec. Assistant turns count under sync_exec unconditionally. On denial it raises a billing error (402 / 403 / 429) before any rows are written. See credit impact.Persist and charge
running_id to the new run_id, commits, and charges exactly one run credit for the turn. Any error on this path releases the admission reservation so it does not leak.Execute in the background
profile="assistant" and workflow_id=None. The response returns immediately with a stream_url you open to watch the turn.POST /assistant/chat response gives you everything you need to start streaming:
Termination and limits
A turn’s loop ends in exactly one of four ways. Three are terminal for the run; one (HITL pause) keeps the chat alive awaiting your response.Natural completion — the model stops calling tools
Natural completion — the model stops calling tools
done event carrying the final response, the list of tool_calls, and a usage object (input_tokens, output_tokens, total_tokens, llm_calls). For the Assistant, has_workflow_changes is always false and workflow_tool is always null (the composer-shaped keys are present but empty). The run status becomes completed and running_id is cleared.Human-in-the-loop pause — the model asks you a question
Human-in-the-loop pause — the model asks you a question
ask_user_* or request_credential), which fires an interrupt. The Assistant writes an interrupt-audit row (outcome="pending"), publishes a user_input_request event, sets a server-side pending sentinel (TTL 7 days), flips run status to interrupted, appends a history-only interrupted marker, and cold-exits the loop awaiting resume. The chat stays open — this is not a terminal run for the conversation. You answer with POST /assistant/chat/{chat_id}/resume, which mints a new run_id and re-enters the same thread. See human-in-the-loop and the HITL resume reference.Cancellation — you stop the turn
Cancellation — you stop the turn
POST /assistant/chat/{chat_id}/cancel sets a server-side cancel flag, publishes a cancelled event, and sets run status to cancelled. If the run was paused on a HITL interrupt, cancel also clears the pending sentinel and flips the audit row to cancelled so the question is not re-presented. Then running_id is cleared. Calling cancel with no active run returns 400 (No active execution to cancel).Error — the turn fails
Error — the turn fails
error event with a sanitized message, followed by a done event with error: true, and sets run status to failed. Language-model token usage accrued before the failure is still recorded (token billing is independent of success).Loop limits
The loop is bounded so it cannot run forever, and it emits guidance long before it hits the ceiling.guidance event ({message, consecutive_failures}) so the model — and you — know something is going wrong. The counter resets on the next successful tool call. This nudges the loop, it does not terminate it.guidance event ({message, total_llm_calls}) encouraging the model to converge. This is advisory, not a hard stop.guidance event is informational. It signals that the loop is struggling (repeated failures) or running long (many LLM calls), but it does not end the turn. Only done, cancelled, error, and the HITL interrupted pause change the run’s lifecycle.Events emitted during a turn
The turn streams as Server-Sent Events. Each frame is a singledata: <json> line with no event: line — discriminate on the JSON type field. The payload for most events is nested under a data key, for example {"type":"response_chunk","data":{"chunk":"..."}}. See streaming responses for how to consume the stream in the app and the SDKs, and the SSE run streaming reference for the transport details.
Credit impact
The Assistant meters usage like any other managed run. Two distinct charges apply per turn:One run credit per turn
/resume (not /chat), so a resume does not add a second run charge for the same turn.Token usage, billed separately
When a turn is denied
The Assistant is one of the surfaces where the billing admission gate is live. If your organization’s allowance and wallet cannot cover the turn, the gate denies it before any rows are written and the request fails with a flatDenialEnvelope — note this has no detail wrapper, unlike standard FastAPI errors:
layer field maps to the HTTP status: credit and wallet to 402, quota to 403, and rate to 429 (with Retry-After and X-RateLimit-* headers). For the full envelope taxonomy and how it differs from the {detail} shape, see errors and status codes; for the gating rules and per-plan limits, see usage gating and limits and permissions and limits.
A worked example: one turn with a tool call
This walks one full turn end to end: start the turn, stream the loop, watch a tool call and its result, and receive the final answer. Every request usesAuthorization: Bearer mx_live_… plus X-Organization-ID — see authentication. As built today the Assistant requires the owner or admin role in the organization; the retired member role is not a current role.
Start the turn
message must be a JSON string (v1 is text-only — an array or object returns 400). Omit chat_id to start a new chat, or pass it to continue one. If you omit llm, your organization’s saved default is used.Open the stream
stream_url to receive the turn’s events as they happen. Both SDKs expose assistant.listen(chatId, runId) as an async iterator that yields the data payload of each frame.Read the loop in the frames
Handle a pause, if one occurs
user_input_request (for example, to connect a missing credential), the run pauses and the stream stays open on heartbeats. Answer with assistant.resume(...) — which mints a new run_id you then open a fresh stream on. The full request and response kinds are in human-in-the-loop.message field must be a JSON string; an array or object returns 400. Continuing a chat that belongs to another organization returns 404 (ownership is hidden as not-found, not 403). A second turn while one is pending or running returns 409.