> ## 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.

# Detect and Handle Conversation Loops in AI Agents

> Use the loop-check endpoint to catch a customer and your agent re-asking each other the same question, hand the conversation to a human, label the failure, and gate promotions on loop-free behavior.

# Detect and Handle Conversation Loops in AI Agents

A conversation "loops" when a customer keeps re-asking the same question and
the agent keeps replying without moving the thread forward. That pattern is
the single most damaging failure an AI concierge can produce — the customer
walks away frustrated, and nothing on the agent side noticed. Orbit ships a
deterministic loop detector that reads the recent message window and answers
"is this conversation stuck?" on demand; the inbox surfaces that verdict as
the supervisor **"conversation in a loop"** banner. This guide shows how to
read the evaluation, hand the thread to a person, and gate agent promotions
on loop-free behavior.

Endpoint paths below are relative. Send them against
`https://api.orbit.devotel.io/api/v1`.

## 1. Where loop-check fits in the supervisor loop

`POST /agents/conversations/:conversationId/loop-check` reads the recent
message window and returns a `LoopEvaluation`:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/agents/conversations/conv_9f2a/loop-check \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{}'
```

The response payload (below, the `data` object of the standard `data`/`meta`
API envelope) carries the verdict plus the evidence your supervisor needs:

```json theme={null}
{
  "conversation_id": "conv_9f2a",
  "evaluation": {
    "isLoop": true,
    "reasonCodes": ["semantic_re_ask", "negative_sentiment_delta"],
    "similarityPeak": 0.71,
    "reAskPair": {
      "earlier": "What is my order status?",
      "later": "Can you check my order status?",
      "similarity": 0.71
    },
    "sentimentDelta": -0.6,
    "customerTurnCount": 4,
    "recommendedAction": "offer_handoff"
  },
  "window_turns": 5,
  "evaluated_at": "2026-09-03T10:00:00.000Z"
}
```

The inbox supervisor view polls this endpoint while a banner is visible,
which is why "conversation in a loop" can show up before a human reviews the
thread. The endpoint is read-only: it never mutates the conversation or the
agent. Handoff is a separate, explicit call (section 3), so the detector
stays safe to run on a poll cadence.

The conversation id can point at either an AI-agent thread or a human-inbox
thread — the route resolves both.

## 2. What the detector looks at

Three signals feed the verdict; any one of them is enough to set
`isLoop: true`. Reason codes stay stable so analytics can group them:

* **`semantic_re_ask`** — two customer turns inside the window share a
  token-overlap similarity at or above the threshold (default 0.55). This
  catches "what's my balance" / "tell me my balance please" / "balance
  again?" — the customer repeating the same request in slightly different
  wording while the agent re-answers the same way. The matching pair comes
  back in `reAskPair` so the supervisor sees the exact two messages.
* **`negative_sentiment_delta`** — the earliest customer turn in the window
  scored neutral-or-positive and the latest turn fell to the negative
  threshold (default −0.3). That is the escalation-from-polite-to-exasperated
  shape a stuck loop shows.
* **`explicit_escalate_keyword`** — the newest customer turn contains a
  direct ask to reach a person ("talk to a human", "real person", "this
  isn't working"). This short-circuits everything else; respect the stated
  preference immediately.

`recommendedAction` maps those signals to a next step:

* `none` — no loop; keep the agent on it.
* `offer_handoff` — re-ask similarity fired; prompt the customer or the
  supervisor to take over.
* `escalate_now` — explicit keyword or a sentiment crater; handoff now.

Signal thresholds are tunable per request (bounded to keep the response
budget clean):

| Field                          | Default | Bounds |
| ------------------------------ | ------- | ------ |
| `window_turns`                 | 5       | 2–20   |
| `jaccard_threshold`            | 0.55    | 0–1    |
| `sentiment_negative_threshold` | −0.3    | −1..1  |
| `min_customer_turns`           | 2       | 1–20   |

## 3. Operator handoff — take over, override, or mark a ticket

A loop verdict without a handoff path leaves the customer stuck in the same
place. Wire the banner and the poll to three actions:

1. **Take over with live-summary.** A supervisor opens
   `GET /inbox/conversations/:conversationId/live-summary?window_turns=N`
   to get a rolling 1–2 sentence AI summary of the last turns, then either
   takes the conversation over in the inbox or releases the thread back to
   the agent after a fix. The summary is opt-in per tenant for AI features.
2. **Override with the handoff endpoint.** When the evaluation is
   `escalate_now` (or a human decides anyway), fire
   `POST /conversations/:id/handoff` with a reason. That clears the AI
   assignment, flips the conversation back to the unassigned queue, and
   stamps the reason category on the row so a report can aggregate loop
   handoffs separately from arbitrary handoffs. The receiving operator then
   picks the thread up from the queue rather than from the agent's surface.
3. **Mark a ticket for review.** Not every loop warrants a human
   interruption. Submit the conversation to the agent's labeling queue with
   `POST /agents/:agentId/labeling/annotations` so QA and fine-tuning pick
   up the failure later (section 5).

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/conversations/conv_9f2a/handoff \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Customer is stuck re-asking order status — loop detector fired",
    "target_queue": "Customer Support"
  }'
```

The supervisor polling cadence (30 s while the banner shows) matches the
endpoint's read budget, so a banner-visible poll is the intended pattern —
not a new evaluation every message.

## 4. Gate promotions on loop behavior, not just quality

Loop-free behavior belongs in the pre-promotion gate next to the model and
quality checks. The rollout pipeline already runs persona simulation, shadow
comparison, and regression tests before a candidate reaches traffic — wire
loop-check into the same stage gates so a candidate that loops on the golden
conversations halts before the rollout advances.

When you run the [AI agent rollout pipeline](/guides/ai-agent-rollout-pipeline),
add loop evaluation to the stage checklist before advancing:

1. Save the candidate as an agent version (same as for regression tests).
2. Replay the saved regression conversations against the candidate.
3. Post the loop-check on each replayed conversation's id and require
   `isLoop: false` before you advance the stage.

A candidate that passes the general evals but loops on the exact
conversations you already marked problematic is a worse deploy than a clean
reject — the detector turns that into a crisp gate.

## 5. How labeling picks up the pattern — loops are a symptom, not a bug

Treat a loop as a signal that the agent's knowledge, prompt, or tooling is
missing something the customer needs. The route is deliberately
deterministic — it does not ask an LLM "is this a loop," so it can run on
every conversation cheaply — which also means the pattern that produced the
loop names its own remedy.

Labeling moves the failure into the eval/fine-tune pipeline where the fix
actually lands:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/agents/agent_abc123/labeling/annotations \
  -H "X-API-Key: dv_live_sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "conversation_id": "conv_9f2a",
    "intent_label": "handoff",
    "quality_score": 2,
    "corrected_response": "I see your order 42 shipped yesterday at 3:12 PM — let me confirm the tracking link.",
    "notes": "Customer re-asked status 3 times; agent never got the tracking from the order tool."
  }'
```

Each label gets appended to the agent's **Human-labeled review queue**
dataset, so an eval run over that dataset, or a fine-tuning export, re-tests
the corrected handling automatically. Use the checker's own reason codes as
notes so analytics can group how loops resolve (missing tool, missing
knowledge, over-long prompt).

The conversation is the symptom, not the bug: once you collect loops from
the labeling queue, the eval pipeline shows whether the prompt edit, the
knowledge change, or the new tool closed the pattern.

## 6. Example: poll loop-check, hand off when the verdict lands

A sideband supervisor script polls loop-check on the conversations an agent
is handling, and hands the thread to the queue when the verdict says loop:

```bash theme={null}
while IFS= read -r convId; do
  evalJson=$(curl -s -X POST \
    "https://api.orbit.devotel.io/api/v1/agents/conversations/${convId}/loop-check" \
    -H "X-API-Key: dv_live_sk_..." \
    -H "Content-Type: application/json" -d '{}')
  isLoop=$(echo "$evalJson" | jq -r '.data.evaluation.isLoop')
  if [ "$isLoop" = "true" ]; then
    curl -s -X POST \
      "https://api.orbit.devotel.io/api/v1/conversations/${convId}/handoff" \
      -H "X-API-Key: dv_live_sk_..." \
      -H "Content-Type: application/json" \
      -d '{"reason":"Loop detector verdict: re-ask threshold exceeded","target_queue":"Customer Support"}'
    curl -s -X POST \
      "https://api.orbit.devotel.io/api/v1/agents/agent_abc123/labeling/annotations" \
      -H "X-API-Key: dv_live_sk_..." \
      -H "Content-Type: application/json" \
      -d "{\"conversation_id\":\"${convId}\",\"intent_label\":\"handoff\",\"quality_score\":1,\"notes\":\"auto-loop detector fired\"}"
  fi
done <<EOF
conv_9f2a
conv_a17b
EOF
```

The pattern stays simple: read the `evaluation.isLoop` boolean, hand off via
the conversations endpoint, and label the failure for the agent's review
queue. Nothing here alters the agent configuration or the conversation row
inside the loop-check call itself — the mutation is the explicit handoff and
the separate annotation.

Keep the poll to the same cadence the supervisor banner uses, and tune the
window once (section 2) instead of per request.

## Troubleshooting

| Symptom                                          | Fix                                                                                                                                                              |
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `404 NOT_FOUND` from loop-check                  | The conversation id is not in the tenant — re-check the id the inbox presented (agent thread vs human thread both resolve here).                                 |
| `isLoop: false` but the customer is stuck        | Widen `window_turns` toward 20 so the detector sees older re-asks, or lower `jaccard_threshold` to recognize looser rewording of the same question.              |
| Detector answers but the banner never appears    | The supervisor poll only runs while the operator UI has the conversation open. Poll the endpoint yourself (section 6) for the headless path.                     |
| Handoff reports “thread was already handed off”  | Another operator or the runtime escalated first — the label queue still records your annotation; fetch `GET /agents/:agentId/labeling/annotations` to reconcile. |
| Labels posted but the eval dataset shows nothing | Resolve the `agentId` you posted to; labels attach to the per-agent “Human-labeled review queue” dataset under that agent.                                       |

## See also

* [AI Agent Rollout Pipeline](/guides/ai-agent-rollout-pipeline) — promotions
  gated on simulation, comparison, and regression results, now also on
  loop-free behavior.
* [Human-in-the-Loop Oversight](/agents/human-in-the-loop-oversight) —
  supervisor takeover, override, and release on live conversations.
* [Live Conversation Monitor](/agents/live-monitor) — the operator surface on
  which the loop-check banner and the labeling queue both live.
* [Continuous Production Evals](/agents/continuous-production-evals) — after
  launch, production sampling keeps testing the same conversations the loop
  detector reads.
