Designing Escalation Paths When an Agent Gets Stuck
An escalation path is a designed route from "the agent cannot safely proceed" to a human (or higher-privilege process) with enough context to finish the job.
Stuck means low confidence, conflicting evidence, repeated tool failure, policy ambiguity, or a required capability outside the current privilege ring.
Summary
Detect stuck states with host rules, stop guessing, package a concise escalation brief, route it to the right queue or person, pause or degrade gracefully, and resume only with a recorded human decision or handoff outcome.
Recipe
- Define stuck signals: no-progress, low confidence, schema fail, policy conflict, missing data, auth errors.
- Cap retries per signal; escalate instead of infinite polite loops.
- Build an escalation brief: goal, attempts, evidence, question for the human, urgency.
- Route by skill and risk (L1 support vs engineering vs compliance), not one mega-inbox.
- Freeze side effects while waiting; keep the user informed with a clear status.
- Accept structured human replies: answer, approve tool, reassign, or abort.
- Resume the agent with the human input as a trusted observation labeled as human, or transfer ownership fully.
- Measure time-to-escalate, time-to-resolve, and reopen rate; tune thresholds.
Working Example
from dataclasses import dataclass, field
from enum import Enum
from typing import Any
class StuckReason(str, Enum):
NO_PROGRESS = "no_progress"
LOW_CONFIDENCE = "low_confidence"
POLICY = "policy_ambiguity"
TOOL_ERRORS = "tool_errors"
NEED_HUMAN_DATA = "need_human_data"
@dataclass
class EscalationBrief:
run_id: str
reason: StuckReason
goal: str
attempts: list[str]
question: str
urgency: str = "normal" # normal | high
evidence: dict[str, Any] = field(default_factory=dict)
QUEUE: list[EscalationBrief] = []
def should_escalate(
*,
identical_tool_hits: int,
confidence: float,
consecutive_tool_errors: int,
needs_user_secret: bool,
) -> StuckReason | None:
if needs_user_secret:
return StuckReason.NEED_HUMAN_DATA
if identical_tool_hits >= 3:
return StuckReason.NO_PROGRESS
if consecutive_tool_errors >= 3:
return StuckReason.TOOL_ERRORS
if confidence < 0.45:
return StuckReason.LOW_CONFIDENCE
return None
def escalate(brief: EscalationBrief) -> dict[str, Any]:
QUEUE.append(brief)
return {
"status": "escalated",
"reason": brief.reason.value,
"queue_depth": len(QUEUE),
"user_message": (
"I need a human teammate for this step. "
f"Question: {brief.question}"
),
}
def route_queue(brief: EscalationBrief) -> str:
if brief.reason is StuckReason.POLICY:
return "compliance"
if brief.urgency == "high" or brief.reason is StuckReason.TOOL_ERRORS:
return "oncall"
if brief.reason is StuckReason.NEED_HUMAN_DATA:
return "requesting_user"
return "support_l1"
# Demo: agent detects thrash and escalates instead of guessing
reason = should_escalate(
identical_tool_hits=3,
confidence=0.9,
consecutive_tool_errors=0,
needs_user_secret=False,
)
assert reason is StuckReason.NO_PROGRESS
brief = EscalationBrief(
run_id="run-42",
reason=reason,
goal="Apply loyalty credit to account A-9",
attempts=["get_account", "get_account", "get_account"],
question="Account lookup returns 404. Correct account id?",
evidence={"account_id_tried": "A-9"},
)
print(escalate(brief))
print("route:", route_queue(brief))Deep Dive
Escalation is not the same as approval
| Pattern | Trigger | Human job |
|---|---|---|
| Approval gate | Agent knows the action; policy requires consent | Allow / deny / edit a concrete tool call |
| Escalation | Agent should not decide alone | Provide missing judgment, data, or ownership |
| Kill switch | Run is harmful or out of control | Stop everything |
Do not force every uncertainty through an "approve send" UI. Ask a clear question when the need is information or judgment.
Detection belongs in the host
Useful host-side detectors:
- Same tool+args signature N times
- Confidence head or verifier score below threshold (if you have one)
- Validator / schema failures after repair budget
- Tool error classes that are not transient
- Explicit model tool:
escalate_to_human(question)(still enforce caps so it cannot spam)
Prefer code thresholds over hoping the model says "I'm stuck."
The escalation brief
Humans abandon walls of logs. Keep briefs short:
- What the user wanted
- What the agent tried (3-7 bullets)
- What blocked progress
- Exact question or decision needed
- Links to trace / ticket
- Deadline / urgency
Store the brief as structured data so routing and SLAs work.
Routing and ownership
Map reason codes to queues. Compliance questions should not sit in a general engineering Slack forever.
For customer-facing agents, distinguish:
- Ask the end user (missing preference, confirmation of identity within policy)
- Ask internal staff (policy edge cases, broken tools, account anomalies)
Never invent answers for identity, legal, or medical edge cases just to avoid escalation.
Resume vs hard handoff
Two valid outcomes:
- Resume: human supplies a fact or decision; agent continues under the same run id.
- Hard handoff: human takes the ticket; agent marks
handed_offand stops tools.
Hard handoff is better when the remaining work is relationship-heavy or out of tool scope.
Confidence without theater
If you use model-verbalized confidence, calibrate it - models are often overconfident.
Pair verbal confidence with independent checks (retrieval agreement, regex/business rules, second model vote on critical paths) before you trust "low confidence" alone.
Gotchas
- Silent guessing. The agent fills missing fields with plausible defaults and ships wrong actions.
- Escalation spam. No budgets; every minor ambiguity pages humans until they mute the channel.
- Transcript dump as handoff. On-call gets 40 tool payloads and no question.
- Wrong queue. All escalations land on one engineer regardless of skill.
- Side effects continue while waiting. Orders still place during "waiting on human."
- No user-visible status. Customers see a spinner with no ownership.
- Treating reject-from-approval as the only escalation. Stuck states happen on pure reads too.
Alternatives
| Approach | Best for | Trade-off |
|---|---|---|
| Structured escalate tool + queue (this recipe) | Product agents with ops teams | Needs staffing and SLAs |
| Always ask the end user | Consumer bots, preferences | Wrong for internal policy |
| Auto-degrade to FAQ / search only | High volume, low risk | May under-serve complex cases |
| Multi-agent specialist handoff | Clear skill splits | Still needs human path when all fail |
| Page on-call for everything | Early prototypes | Unsustainable; burns trust |
FAQs
When should the agent escalate vs retry?
Retry transient network and rate-limit errors with caps. Escalate no-progress, policy ambiguity, and missing privileged data.
Should escalation require the same auth as approval gates?
Internal escalations need authenticated staff. End-user questions need session auth. Do not accept anonymous "just approve it" messages.
How do we keep latency acceptable?
Set user-visible expectations, allow partial answers that are safe, and staff queues to your SLA. Async agents should not pretend to be finished.
Can we escalate to another agent instead of a human?
Yes as a first hop (specialist). If the specialist is also stuck, escalate to a human rather than cycling forever.
What belongs in user-facing copy?
Honest status and the question you need. Avoid dumping internal tool errors or policy codes unless product design requires it.
How does this interact with privilege rings?
Needing an R2+ capability without approval is a form of stuck/escalation. Route to an approver with a decision packet, not a free-text chat only.
How do we test escalation paths?
Inject stuck fixtures (404 loops, low confidence, policy flags) and assert: no further side effects, brief fields present, correct queue, resume works.
What metrics prove the design works?
Escalation rate, median time-to-first-human-response, resolution without reopen, and incidents where the agent guessed instead of escalating.
Is "I don't know" an acceptable model output?
Yes when paired with a host path that escalates or asks. Bare "I don't know" with no routing is a dead end.
Where should we document paths?
In a short matrix: signal → action → owner → SLA. Link it from runbooks and the agent README.
Related
- Human-in-the-Loop Basics - pause and resume basics
- Building an Approval Gate Before Irreversible Actions - consent for known actions
- Kill Switches: Stopping a Runaway Agent Mid-Execution - emergency stop
- Privilege Rings: Scoping What an Agent Can Do Without Asking - capability tiers
- Audit Trails: Logging Every Agent Action for Later Review - evidence packs
- Human-in-the-Loop Best Practices - checklist
Stack versions: Pins from the category manifest (verify at build): OpenRouter (~315+ models, July 2026 pricing/fees); LangGraph 1.0+; CrewAI 1.14+; Microsoft Agent Framework 1.0; Vercel AI SDK 6; Pydantic AI (latest); LlamaIndex (latest); OpenAI Agents SDK (latest + MCP); MCP (Linux Foundation governance); A2A (HTTP+SSE+JSON-RPC 2.0); Solana
@solana/web3.js+@solana/spl-token.