Gateway Turn Error Codes
Typed error payload on the final response frame when a gateway turn fails
When a turn fails, the final response frame carries a typed outcome.error, so a chat UI can show the right Retry or Stop button and a safe message instead of a raw provider traceback.
Quick Start
- Read outcome.error on a final response
python import asyncio from praisonai_bot.gateway.client import GatewayClient async def main(): client = GatewayClient(url="ws://localhost:8765", agent_id="my-agent") await client.connect() await client.send("Summarise today's news") async for event in client.events(): frame = event.data if event.type == "response" and frame.get("status") == "final": outcome = frame["outcome"] if outcome["status"] == "error": err = outcome.get("error", {}) print(err.get("code"), "-", err.get("message")) break asyncio.run(main()) - Retry only when retryable
python import asyncio from praisonai_bot.gateway.client import GatewayClient async def ask(client, text): await client.send(text) async for event in client.events(): frame = event.data if event.type == "response" and frame.get("status") == "final": return frame async def main(): client = GatewayClient(url="ws://localhost:8765", agent_id="my-agent") await client.connect() for attempt in range(3): frame = await ask(client, "Summarise today's news") outcome = frame["outcome"] err = outcome.get("error") if outcome["status"] != "error" or not err or not err["retryable"]: break await asyncio.sleep(err["retry_after_seconds"] or 2 ** attempt) print(frame["content"]) asyncio.run(main())
How It Works
The gateway classifies the exception, picks a fixed safe message for its code, and attaches the result to the final frame.
{
"type": "response",
"status": "final",
"content": "Error: ...",
"outcome": {
"status": "error",
"error": {
"code": "rate_limit",
"retryable": true,
"retry_after_seconds": 30,
"message": "Rate limited by the model provider. Please retry shortly."
}
},
"session_id": "..."
}| Field | Type | Meaning | |
|---|---|---|---|
code | string | Machine-readable error code (see table below) | |
retryable | bool | true when resending the same message can succeed | |
retry_after_seconds | `int \ | null` | Provider’s Retry-After, rounded up; null when unknown |
message | string | Fixed safe text per code, never raw str(e) |
Error Codes
code | retryable | Typical cause | message | Client action |
|---|---|---|---|---|
rate_limit | true | Provider 429 / rate limit | “Rate limited by the model provider. Please retry shortly.” | Wait retry_after_seconds, then retry |
overloaded | true | Provider temporarily overloaded | “The model provider is overloaded. Please retry shortly.” | Back off, then retry |
transient | true | Timeout or network blip | “A temporary error occurred. Please retry.” | Retry |
idle_timeout | true | Provider stopped streaming | “The model provider stopped responding. Please retry.” | Retry |
context_overflow | false | Prompt exceeds model context | “The conversation is too long for the model. Start a new chat.” | Offer “Start a new chat” |
billing | false | Account out of credit | “The model provider account is out of credit.” | Show top-up link |
auth | false | Credentials rejected (recoverable) | “The model provider rejected the credentials.” | Ask the operator to re-authenticate |
auth_permanent | false | Credentials rejected (permanent) | “The model provider rejected the credentials.” | Ask the operator to replace the key |
model_not_found | false | Configured model unavailable | “The configured model is not available.” | Ask the operator to fix the model |
invalid_request | false | 400-class request error | “The agent failed to complete this turn.” | Show message, do not retry |
internal | false | Anything unrecognised | “The agent failed to complete this turn.” | Show message, do not retry |
Common Patterns
Retry or stop
import asyncio
async def handle(frame):
err = frame["outcome"].get("error")
if err and err["retryable"]:
await asyncio.sleep(err["retry_after_seconds"] or 2)
return "retry"
return "stop"Branch by code
def next_step(err):
code = err["code"]
if code == "context_overflow":
return "Start a new chat"
if code == "billing":
return "Top up your credit"
if code in ("auth", "auth_permanent"):
return "Re-authenticate"
return err["message"]JavaScript
const ws = new WebSocket("ws://localhost:8765");
ws.onmessage = (msg) => {
const frame = JSON.parse(msg.data);
if (frame.type !== "response" || frame.status !== "final") return;
const err = frame.outcome.error;
if (frame.outcome.status === "error" && err?.retryable) {
setTimeout(() => resend(), (err.retry_after_seconds ?? 2) * 1000);
}
};User Interaction Flow
The user sees a spinner, a short “Retrying” notice, then the answer.
Best Practices
Show message, not content
content on a failed turn still starts with Error:. Show outcome.error.message to end users — it is a fixed safe string.
Honour retry_after_seconds
Wait the full retry_after_seconds before resending. Fall back to exponential backoff only when it is null.
Cap your retries
Retry a few times at most, then surface the message. retryable means a retry can succeed, not that it will.
Don't confuse with connect errors
Turn errors leave the socket open and you decide whether to resend. Connect errors close the socket and drive the reconnect loop.