Human takeover
Sometimes a conversation should move from the agent to a person: a sensitive case, an angry customer, a question the knowledge base cannot answer. Human takeover lets an operator own the conversation while the AI stays quiet, then hand it back when the person is done.
Ownership states
Each conversation is either ai_owned or human_owned.
When a conversation is ai_owned, visitor messages follow the normal assistant path — routines, retrieval, skills, model calls, and outbox actions all run.
When a conversation is human_owned, Radioso suppresses the AI turn. New visitor messages are still saved, but the assistant does not run routines, retrieve, dispatch skills, call the answer model, or enqueue outbox actions.
Before a teammate replies, the assistant — the chat surface your agent speaks through — returns one short waiting line instead of an answer — “a teammate is joining, please wait” — generated by the model in the conversation’s language, so it is multilingual. Once an operator has replied in the thread, the teammate has joined: later visitor messages get no AI reply and wait, so the visitor is never told a teammate is “joining” when one is already there. The AI stays silent until an operator hands the conversation back.
How a handoff is requested
Handoff is request-driven. The AI does not silently decide to transfer a conversation. There are two triggers:
- A routine reaches a
handoffterminal — including a branch an LLM-selected transition chooses, such as an authored path for an annoyed user. - An agent has
handoffOnRetrievalMissenabled and a turn produces a no-context grounded miss. This is opt-in per agent and off by default.
Both request human ownership, notify an operator through the contact-delivery transport with a handoff.notify action, and record a hitl.ownership audit event.
Operator console
The dashboard surfaces this work under Activity, which has three sections in its sidebar:

- Needs attention — the operator inbox. Critical escalations (an approval to decide, a handoff awaiting or held by a human) sort to the top. Explicit thumbs-down feedback follows, ordered by the latest feedback creation or edit, then lower-concern quality signals such as degraded or no-context answers. Reviewing feedback opens the exact failed answer with direct paths to add knowledge, improve the agent’s behavior, test again, and resolve or dismiss the item. A compact popover closes the item immediately or records an optional classification; Other is the only choice that asks for a note. Feedback submitted after an earlier resolution or dismissal reopens the work. Passive quality signals stay capped and use the same close flow.
- All activity — the full conversation history.
- Quality — the paginated answer-quality backlog and per-turn triage: negative feedback, slow responses, and skill failures, alongside the grounding gaps summarized in the inbox. Retrieval answers with a complete diagnostic show the sourced claim total and separate warnings for unsourced claims or invalid citations under the Outcome badge. A no-support answer with zero claims says No supported claims; turns without complete evidence show no diagnostic line. Open Filter → Evidence to select grounding verdicts or focus on unsourced claims and invalid sources. Closed work can be filtered by structured resolution reason and closure time, and the compact resolution breakdown opens the exact matching queue. Filter state stays in the URL, so a review queue can be shared. Add to Eval preserves a weak turn as a repeatable case; once linked, the row shows timestamped verification evidence and the action becomes Open Eval.
GET /api/v1/quality/turns exposes the same detail as a complete grounding object or null. The groundingVerdict, hasUnsourcedClaims, and hasInvalidSources query parameters filter it server-side. A false count-presence filter includes complete zero-count diagnostics and excludes unknown diagnostics.
A turn counts as a grounding gap only when the agent tried to ground an answer and found nothing. When it declines because the question sits outside what its instructions cover — the capital of Mars, a maths puzzle, an attempt to talk it out of its own remit — the turn carries the Out of scope action and sits on neither side of the grounded-answer rate. That leaves the gap queue holding the questions actually worth ingesting content for.
The conversation view shows message attribution (a badge for human-agent and system messages) and an operator action bar: take over, reply, hand back, and approve or reject a pending decision. While it is open it reads the tail endpoint, so new visitor messages and your own replies appear without a manual refresh.
Operator API
You can act from the console or directly through authenticated endpoints under /api/v1/conversations. All of them require a workspace session with the workspace.conversation.takeover permission, and every action records a hitl.ownership audit event.
Take over
POST /api/v1/conversations/{conversationId}/takeover{ "reason": "operator_takeover" }reason is optional. The response returns the current ownership record.
Reply as a human
POST /api/v1/conversations/{conversationId}/reply{ "message": "Thanks for waiting. I can help with this.", "expectedVersion": 3 }The reply is saved as an assistant-role message with source: human_agent and carries the operator identity in metadata. expectedVersion must match the current human-owned ownership record; if the conversation was transferred or handed back, the endpoint returns 409 with the current record in error.details.ownership.
Transfer ownership
POST /api/v1/conversations/{conversationId}/transfer{ "toAccountId": "00000000-0000-0000-0000-000000000000", "expectedVersion": 3 }expectedVersion is an optimistic-concurrency token from the ownership record; a stale value returns 409 with the current record. toAccountId must own the conversation’s workspace — targets outside it are rejected.
Hand back to the AI
POST /api/v1/conversations/{conversationId}/handback{ "expectedVersion": 4 }After hand-back, the next visitor message follows the normal assistant path again.
Approvals
A routine can pause at an approval gate before a step with side effects. When a turn reaches the gate, the routine suspends, the assistant replies that the step needs review, and Radioso records a pending decision. The conversation is not handed over — it waits for an operator to decide. See Author a routine for how to author the gate.
List pending approvals
GET /api/v1/decisionsReturns the workspace’s open approval decisions, newest first. Each carries what an operator needs to decide and submit a resolve: handle, conversationId, agentId, routineId, stepId, reason, options, contentHash, deadline, and createdAt. Requires the workspace.conversation.takeover permission.
Resolve an approval
POST /api/v1/agents/{agentId}/decisions/{handle}/resolve{ "optionId": "approve", "contentHash": "sha256:...", "payload": null }optionId must be one of the decision’s options. contentHash must match the value from the pending decision; a stale hash returns 409. The decision flip, the routine resume, and the resumed turn commit in one transaction, so a crash before commit leaves the decision pending and a retry resolves cleanly. Approving resumes the routine and lets the gated action run; rejecting takes the rejection branch. A gated side effect runs as an idempotent outbox action, never inline.
Live updates
Both surfaces can read forward for new messages instead of refetching the whole transcript:
GET /api/v1/history/chat/{conversationId}/tail?cursor=... (operator)
GET /api/v1/public/chat/{token}/tail/{conversationId}?cursor=... (visitor)
GET /api/v1/public/chat/{token}/events/{conversationId} (visitor push, SSE)Each tail call returns messages created after the cursor plus an advanced cursor. With no cursor, tail returns the newest bounded page. Messages include source, so a visitor sees a human reply distinctly, plus operatorDisplayName on a human-agent reply so the visitor can see who is answering — only the name is exposed, never the operator’s account id. The operator tail also includes ownership; the visitor tail never does.
Common failure modes
- A
409on reply, transfer, or handback means the ownership record moved since you read it. Re-read the current ownership fromerror.details.ownershipand retry with the newexpectedVersion. - The AI keeps answering a conversation you meant to own: no one has taken it over yet. Ownership flips only on an explicit takeover, a routine handoff, or a retrieval-miss handoff.
- A transfer is rejected:
toAccountIdmust be an account that owns the conversation’s workspace. - The visitor never sees a “joining” line: the line is model-generated, and a conversation where an operator has already replied gets no waiting line at all — later messages just wait.
Read next
- Author a routine — author the handoff terminals and approval gates that trigger takeover.
- Slack channel — approve, take over, and reply from Slack.
- Clarification behavior — how the agent asks before it escalates.