Human-in-the-Loop
Human-in-the-Loop Escalation Gating¶
When a policy decision is escalate, the agent can now block and wait until a human reviews the action on the Authorisation Layer dashboard. This is controlled by the wait_on_escalate parameter on the @guard decorator (default: True).
Full Flow¶
- Agent calls the guarded function → Authorisation Layer intercepts and returns
decision: "escalate"with anescalation_id. - SDK polls
GET /v1/enforce/escalations/{id}/statuseveryescalation_poll_intervalseconds. - Human opens Authorisation Layer → Control Plane, sees the pending escalation card with Approve and Reject buttons.
- If approved: agent unblocks and the wrapped function executes normally.
- If rejected (or timeout exceeded):
AgentBlockedErroris raised and the function never runs.
Code Example¶
from xybern import Xybern, AgentCredential, AgentBlockedError
client = Xybern(api_key="xb_your_key")
cred = AgentCredential.load("./bot.cred")
@client.agents.guard(
credential=cred,
wait_on_escalate=True, # Block until human decides (default)
escalation_timeout=300, # Wait up to 5 minutes
escalation_poll_interval=5, # Check every 5 seconds
)
def wire_transfer(amount_usd: float, recipient: str):
# Only executes if a human approves on the Authorisation Layer dashboard
execute_wire(amount_usd, recipient)
try:
wire_transfer(50000, "vendor@example.com")
except AgentBlockedError as e:
print(f"Transfer rejected or timed out: {e}")
InterceptResult Attributes¶
| Attribute | Type | Description |
|---|---|---|
.decision |
str |
"allow", "block", or "escalate" |
.trust_score |
float |
Agent trust score at time of decision (0-100) |
.decision_id |
str |
Immutable decision record ID |
.escalation_id |
str | None |
Set when decision == "escalate"; used to poll for human resolution |
.policy_name |
str | None |
Name of the policy that triggered this decision |
.latency_ms |
int |
Decision latency in milliseconds |
wait_for_escalation() Method¶
The @guard decorator calls this automatically when wait_on_escalate=True. You can also call it directly when handling escalations manually.
# Manual escalation polling (when wait_on_escalate=False)
result = client.agents.intercept(
action_type="wire_transfer",
action_content="Transfer $50,000 to vendor",
agent_id=cred.agent_id,
)
if result.decision == "escalate":
resolution = client.agents.wait_for_escalation(
escalation_id=result.escalation_id,
timeout=300, # seconds
poll_interval=5, # seconds
)
# resolution == "approved" or "rejected"
if resolution == "approved":
execute_wire(50000, "vendor@example.com")
Escalation Status Endpoint¶
GET /v1/enforce/escalations/{escalation_id}/status
Poll the resolution status of a pending escalation. Returns {"ok": true, "status": "pending"|"approved"|"rejected"}. Used internally by the SDK's wait_for_escalation() method.
Dashboard: Approve / Reject¶
Pending escalations appear as cards in Authorisation Layer → Control Plane. Each card shows the action details, agent identity, and two buttons: Approve and Reject. Resolving a card immediately unblocks any waiting SDK call.
The underlying endpoint is: POST /api/sentinel/enforcement/escalations/{escalation_id}/resolve with body {"resolution": "approved"|"rejected"}.
Info
Set wait_on_escalate=False if you want fire-and-forget escalation behaviour, the SDK returns immediately with decision="escalate" and you handle the escalation_id asynchronously.