> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Orby operator assistant architecture: sessions, turns, tools, and the approval gate

> How Orby — the operator assistant built into the Orbit dashboard — is put together: the operator-session auth boundary, threads and the FSM snapshot, the tool registry with explicit approval, the streaming turn loop, and the two grounding sources.

# Orby operator assistant architecture

Orby is the operator assistant embedded in the Orbit dashboard. It answers questions about your own workspace ("how many conversations are unassigned?"), orients operators ("where do I set quiet hours?"), and proposes data-changing actions behind an explicit approval step. This page explains how the pieces fit — the auth boundary, the thread model, the tool registry and approval gate, the turn loop, and the grounding sources — so you can reason about the whole surface before you integrate or trust it. For the operator experience, see [Using Orby, the in-dashboard operator assistant](/guides/orby-in-dashboard); for the endpoint contract, see the [Orby API reference](/api-reference/orby).

## 1. What Orby is — and what it is not

Orby serves **operators**: the people who run your Orbit workspace day to day. It lives in the dashboard as a chat panel, and everything it does is scoped to the signed-in operator's organization, role, and own threads.

Contrast this with [AI agents](/concepts/ai-agent-architecture), which are **customer-facing**: an AI agent is deployed to a messaging channel or a voice number and holds conversations with your customers, driving run lifecycles on your behalf. Orby never talks to your customers. When Orby acts, it acts as the operator's hands — drafting a message the operator approves, reassigning a conversation, pausing a campaign — never as an autonomous party in a customer dialogue. The two surfaces share infrastructure (grounding, guardrails, the approval discipline) but answer a different user: one for the person running the workspace, one for the conversation itself.

## 2. The auth boundary: operator sessions only

Every Orby request passes through Orbit's shared authentication middleware, which accepts **either** a dashboard session token **or** an API key. Orby then layers a second guard on top: it rejects any request which authenticated with an API key, returning `403 ORBY_SESSION_REQUIRED` before any route handler runs.

Why the extra layer? The shared authentication layer is deliberately permissive — most of the API serves both dashboard sessions and server-to-server callers, and it resolves an API key first, falling back to a Clerk session only when no key is present. Orby deliberately narrows that. Orby's tool registry can execute write-scoped actions (send an SMS, reassign a conversation, pause a campaign) and its knowledge surface reads workspace data. Pinning Orby to a signed-in operator session means:

* A leaked or over-permissioned API key cannot drive Orby's tool execution or knowledge-base surface at all — the request is rejected regardless of what the key itself is allowed to do.
* Every turn carries a concrete operator identity: threads, pending actions, approvals, and the audit trail all attach to a person, not an anonymous server credential.
* Orby inherits exactly the operator's role — it can reach nothing the operator couldn't reach through the dashboard itself. There is no separate assistant identity and no hidden privilege.

## 3. The thread model and the FSM snapshot

A conversation with Orby is a **thread**: an ordered list of messages belonging to the operator who created it. Threads persist across sessions and are listed newest-first in the "Past chats" panel; an operator's threads are private to that operator, and never visible to teammates. A new thread is auto-titled from the first message by a small title-generation pass, so the panel stays navigable.

Between turns, the conversational state of a thread is carried by a **FSM (finite-state machine) snapshot**. The snapshot records where the current exchange sits — most importantly, whether the turn is idle or parked on a pending tool action awaiting the operator's decision. Snapshots are loaded at the start of a turn and saved as it progresses. Persistence is layered:

* a Redis hot path with a short TTL, so the snapshot evicts itself when the operator walks away;
* a Postgres fallback written alongside, so a Redis blip mid-turn never loses an in-flight pending action.

Snapshot payloads are validated at the boundary, so a stale or drifted snapshot shape drops safely back to the idle state rather than corrupting the turn.

## 4. The tool registry and the approval gate

Orby's side effects run through a single shared **tool registry**. Each registered tool declares a name, an argument shape, and — the load-bearing property — a **confirmation policy**:

* Read-only tools (counts, searches, summaries, drafts) have a never-confirm policy. They execute inline in the turn and return their result immediately.
* Data-changing tools (send a message, reassign a conversation, pause or resume a campaign, scaffold a draft agent) require confirmation. When a turn invokes one, Orby **pauses** instead of executing: the turn proposes the action, renders it as a pending-action card with the exact arguments and an estimated cost, and waits.

The operator resolves the proposal two ways, and both land in the same place: click **Approve** / **Reject** on the card, or simply reply in chat. A classifier inspects a bare follow-up message against the open pending action — "yes", "confirm", "cancel" — and routes it to the same approve/reject path as the buttons; anything that isn't a bare confirm or cancel falls through to the normal turn flow with the pending action in context.

The gate itself is disciplined:

* **Expiry.** A pending action expires after 5 minutes; the countdown elapsing cancels it and nothing runs.
* **Replay and race safety.** Approving an already-resolved action is rejected, and a concurrent second approval is refused.
* **Regrounding on approval.** Between proposal and approval the tool arguments are re-read from the tenant data — if the conversation you were reassigning was already moved by a teammate, the approval refuses to run on stale inputs instead of acting on a snapshot of the past.
* **Scope.** Proposed actions run under the [authorization-mandates framework](/agents/authorization-mandates) — scoped, capped, and revocable grants verified at act time. Nothing executes outside that gate.
* One registry, everywhere. The turn route that proposes an action and the confirm route that redeems it read the same registry instance, so a tool that can be proposed can always be decided.

### An approval turn, end to end

```mermaid theme={null}
sequenceDiagram
    participant Operator
    participant Dashboard
    participant Assistant as /assistant (SSE)
    participant Registry as Tool registry
    participant Confirm as /tool-actions
    participant Tenant as Tenant data

    Operator->>Dashboard: "Send an SMS to +15555550100 ..."
    Dashboard->>Assistant: POST turn (Clerk session)
    Assistant->>Registry: resolve tool + requireConfirmation
    Registry-->>Assistant: write-scoped tool
    Assistant-->>Dashboard: SSE: tool_pending (id, args, ETA cost)
    Dashboard-->>Operator: pending action card
    Operator->>Dashboard: clicks Approve (or replies "yes")
    Dashboard->>Confirm: approve pending action
    Confirm->>Tenant: reground arguments (re-read live data)
    Tenant-->>Confirm: fresh arguments
    Confirm->>Registry: execute tool
    Registry-->>Confirm: result
    Confirm-->>Dashboard: action resolved
    Dashboard-->>Operator: card shows outcome (later: undo if within window)
```

## 5. Turn execution: the SSE loop and its limits

A turn is `POST /api/v1/orby/assistant` — you send the message, and the response arrives as a **Server-Sent Events (SSE) stream** rather than a single JSON body. Token deltas arrive as they are generated, tool calls surface as events, and a `tool_pending` event is emitted the moment a turn pauses on an approval so the client can render the card. Streaming keeps long grounded answers interactive; the [API reference](/api-reference/orby) documents the event shapes.

Two protections bound the loop:

* **Per-operator rate limit.** Turns are limited to **60 per minute per operator**, keyed on the operator identity, enforced against a Redis counter with a bounded in-process fallback so a Redis outage degrades the limiter proportionally rather than opening a hole. Hit the limit and Orby tells you how long to wait.
* **Degraded reads, honest writes.** When the backing store is briefly unavailable (a database failover, an upstream auth resolution outage), the two idempotent thread reads degrade instead of failing in the browser: the thread list renders as the empty list, and reopening a thread renders as an empty message list — the same shapes those endpoints return for an operator with no threads yet. Nothing is deleted by a degraded view; once the store recovers, the real threads are back. The write side never degrades: sending a new turn still reports a real error if the backend is down, because a failed turn must never pretend it ran.

## 6. Grounding: workspace data and the documentation index

Orby's answers draw from two grounding sources, in addition to the page the operator is looking at (the panel carries the current dashboard route as starting context):

1. **Live tenant data.** Operational questions — counts, statuses, a specific conversation — are answered from the operator's own tenant records, read live by the read-only tools in the registry and cited in the reply, scoped to what the operator's role can already read.
2. **The Orbit documentation knowledge base.** The same corpus this site is built from is indexed for hybrid search. "Where do I configure X" questions resolve to the dashboard setting that controls it plus the matching doc page, rather than a paraphrase from memory. The docs search surface is the one under `/orby/kb` — also reachable directly with Cmd+K anywhere in the dashboard — and it degrades to an empty result with a signal while the index is cold, instead of erroring.

When neither source can ground an answer, Orby asks a clarifying question rather than guessing.

## Cross-references

* [Orby API reference](/api-reference/orby) — endpoint shapes for turns, threads, and tool confirmations.
* [Using Orby, the in-dashboard operator assistant](/guides/orby-in-dashboard) — the operator workflow on top of this architecture.
* [AI agent architecture](/concepts/ai-agent-architecture) — the customer-facing counterpart Orby is deliberately not.
* [Authorization mandates](/agents/authorization-mandates) — the scoped-consent gate every proposed tool action runs through.
* [Operator observability map](/concepts/operator-observability-map) — where Orby's audit trail surfaces among the dashboards, logs, and review surfaces.
