Escalate to human (async handoff)
File a ticket and finish this turn. The human works the case later, elsewhere. This run does not wait.
A Mode 1 domain tool opens a ticket for a human queue, then the parent run completes. That is not a process gate. Use a gate when the same run must continue after an approve or reject. The public index: use cases.
Seed proof: shopassist_case.
Who does what
Lookups → billing/policy domain APIs (optional)
│
▼
CALL escalate_to_human
│ POST domain handoff API (mock: /handoff/escalate)
│ slot escalate_to_human = { handoff_id, text }
▼
Runtime policy (not another LLM turn required)
│ if handoff ok AND no pending specialist joins → complete parent
│ if handoff ok AND joins still pending → keep looping (no second ticket)
▼
Customer-facing result (auto or DONE)
│ e.g. "I've escalated… Reference hof-1. Someone will follow up."
▼
run.terminal status=completed
│
▼
Human queue (outside Fabric)
│ works hof-1 later; does not resume this Pattern 1 loop
Two Runtime guarantees
1. Auto-complete after success (when nothing else is pending)
After a successful escalate (handoff_id present in slots.escalate_to_human):
Pending kind=agent joins? | Behavior |
|---|---|
| No | Parent auto-completes with a customer message that includes the handoff id. Audit shows completed. |
| Yes (child started, not yet terminal join) | Do not complete yet; loop continues until joins finish or the model DONEs. |
“Pending join” means a specialist slot has correlation_id / route_id but is not yet { status: completed|failed, result: … }.
2. Idempotent re-CALL
If the model CALL escalate_to_human again after a handoff already exists:
| Prior handoff? | Pending joins? | Behavior |
|---|---|---|
| Yes | No | No second POST. Complete with the same handoff_id. |
| Yes | Yes | No second POST. Note that the handoff is already open; keep looping. |
| No | — | First POST creates the ticket. |
Never open a second ticket for the same parent turn after success.
Scenario (ShopAssist)
Customer: damaged jacket ORD-77819, possible double charge, wants full refund.
- ASK for locator → lookup order →
investigate_duplicate_charge/check_return_policyas needed. - Policy: eligible but above auto limit → needs human.
escalate_to_human→hof-1.- Parent completes (auto) with the handoff reference.
- Ops works
hof-1later; this chat run is already done.
Chat scratchpad — shopassist_case_ask.
Contrast
escalate_to_human | human_gate | |
|---|---|---|
| Shape | Domain HTTP tool | Workflow stage type=human_gate |
| After success | Parent usually completed | Parent waiting |
| Human continues | Separate queue / ticket | Same correlation via /turns |
| Idempotency | Re-CALL does not create a second handoff | N/A (pause, not a ticket tool) |
Code map
- Loop + auto-complete + idempotency:
agent-runtime/app/graph/workflow.py(_complete_after_escalate,_idempotent_escalate_replay) - Capability / mock: seed
escalate_to_human;agent-fabric-mocks/tools/catalog/escalate_to_human.json - Tests:
agent-runtime/tests/test_graph.py(test_agent_loop_escalate_*)