Skip to content

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 handoff terminal — including a branch an LLM-selected transition chooses, such as an authored path for an annoyed user.
  • An agent has handoffOnRetrievalMiss enabled and a turn produces a no-context grounded miss. This is opt-in per agent and off by default.

In the agent’s Profile → Answers settings, turn on Hand off on retrieval miss to enable this trigger.

Both request human ownership, notify an operator through the contact-delivery transport with a handoff.notify action, and record a hitl.ownership audit event. A routine handoff’s notice names the routine and lists the values it collected; a retrieval-miss notice carries the ids and the reason. The webhook body is documented under Handoff and approval notifications.

Operator console

The dashboard surfaces this work in the Inbox, a top-level sidebar item ahead of Agents, Knowledge Base, Audience Pulse, and Quality, with a badge showing how many items are open.

The Inbox opens with a lens toggle at the top of its left pane. Needs you is the default: the queue of everything waiting on a person — handoffs, approvals, and negative feedback. All lists every conversation, newest first, each row titled with a short topic the model generates from the conversation — falling back to the visitor’s opening message until a topic exists — with an outcome chip (In progress, Completed, or Handed off) plus search and filters for outcome, agent, and site.

In Needs you, the left pane lists open items with search and filters for type, agent, and who has taken each one; critical escalations — an approval to decide, a handoff awaiting or held by a human — sort to the top, then written thumbs-down feedback, ordered by its latest creation or edit. Automatically detected signals and uncommented feedback stay in Quality instead of adding one inbox row per answer.

Both lenses share the same reading pane. Select an actionable conversation — one waiting on a human or already human-owned — and it opens: a one-line header naming the visitor, the page they were on, and how long they’ve been waiting; a situation card with the handoff reason and the visitor’s opening request; the live transcript; and a reply composer that’s always there. Send a reply and you’ve claimed the item — there’s no separate take-over step first. While the item is open, new visitor messages and your own replies appear without a manual refresh. Choose Done to close a handoff and hand the conversation back to the agent; on a negative-feedback item, Done opens the same resolution-reason flow Quality → Review uses to classify it. An approval closes when you choose one of its decision options — it needs no separate Done step. Select any other conversation and the reading pane is read-only, with an outcome footer in place of the composer.

The browser tab title shows how many items are open, and a soft sound plays when a new handoff or approval arrives while you have the dashboard open.

Either lens has an Open in debug view link, which opens the conversation drawer: transcript, Debug, Flow, a button to continue it in test chat, and a button to send it to Eval. The drawer is for inspecting and testing — replying, taking over, handing back, and approving or rejecting a pending decision all happen from the reading pane instead.

The drawer opens with a Visitor panel above the transcript whenever the conversation carries a visitor or a request context: country, region, and city; browser and OS, with the raw user agent in a tooltip; language; entry page; referrer; IP address; and when the visitor first showed up. An “unverified” hint means an edge marker arrived with the request but did not check out, so no facts are shown for it. Below those fields, Previous conversations lists the visitor’s five most recent other conversations with a link to each and a “see all N” count when there are more — this is what groups conversations under the same durable visitor key from the embed, or the same verified customer id, across sessions. A field with nothing to show renders as unknown rather than being hidden, so a thin profile still confirms the panel worked.

Conversation rows across the Inbox and Quality lists show the country code next to the visitor label when a geo header resolved one, ahead of the page or channel it names.

Quality is its own top-level section with two pages. Review is the paginated answer-quality backlog and per-turn triage: negative feedback 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 on the Evals page; 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.

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

text
POST /api/v1/conversations/{conversationId}/takeover
json
{ "reason": "operator_takeover" }

reason is optional. The response returns the current ownership record.

Reply as a human

text
POST /api/v1/conversations/{conversationId}/reply
json
{ "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

text
POST /api/v1/conversations/{conversationId}/transfer
json
{ "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

text
POST /api/v1/conversations/{conversationId}/handback
json
{ "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

text
GET /api/v1/decisions

Returns 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

text
POST /api/v1/agents/{agentId}/decisions/{handle}/resolve
json
{ "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:

text
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 409 on reply, transfer, or handback means the ownership record moved since you read it. Re-read the current ownership from error.details.ownership and retry with the new expectedVersion.
  • 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: toAccountId must 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.