Skip to main content
Failure Reply is the failure-path counterpart of the Visible-Outcome Guarantee: every failed turn — expired auth, missing key, rate limit, timeout, budget, doom-loop, needs-help — reaches the user as a typed, actionable message telling them exactly what to do next.

Quick Start

1

It's on by default

Every adapter calls render_failure_reply at the failure-handling seam — there’s no config knob to enable. A normal agent already benefits:
2

Inspect a failure yourself

Call render_failure_reply on any failed run — an AgentRunOutcome, a PraisonAIError, or a plain exception:
3

Record the reason code

Log reply.reason_code for aggregation. Compare against the REASON_* module constants, not the string literals:
Before this primitive, a failed turn delivered a generic string like Error: 401 Unauthorized — or a silent downgrade. The rich taxonomy in praisonaiagents.errors / praisonaiagents.run_outcome (with retryability and remediation_hint) never reached the user, and every adapter re-invented the mapping.

Which primitive?

classify_final (silence) and render_failure_reply (failure) are easy to confuse — one handles empty finals, the other handles failed runs.

FailureReply

render_failure_reply(outcome) is a single, pure, channel-agnostic decision that maps any failed run into a typed FailureReply.
  • Import: from praisonaiagents.bots import FailureReply, render_failure_reply
  • Signature: render_failure_reply(outcome: AgentRunOutcome | PraisonAIError | Any) -> FailureReply
  • Zero deps. Inputs are duck-typed — an AgentRunOutcome, a PraisonAIError subclass, or any object exposing error_category / status. Anything else degrades to a generic (but still visible) unknown reply — it never raises.

Reason codes

Compare against the REASON_* module constants, not the string literals. REASON_RATE_LIMIT, REASON_OVERLOADED, and REASON_TIMEOUT are the always-retryable set. Other reasons still land as retryable=True when the source outcome’s is_retryable() was affirmative (e.g. invalid_outputformat_error reason, but retryable).

Mapping tables

_ERROR_KIND_TO_REASON maps a PraisonAIError.error_category to a reason code: Special case: if the error carries a config_key and the category is format_error, the reason becomes missing_key — steering the user to onboarding rather than re-auth. _TERMINATION_TO_REASON maps an AgentRunOutcome.context["termination_reason"]: When neither map hits, the fallback ladder is error_categorystatus (timeout / cancelled / invalid_output → format_error) → unknown.

How It Works

The module never imports PraisonAIError / AgentRunOutcome at runtime — it reads fields by duck-typing, keeping it a zero-dependency leaf. A concrete remediation_hint on a PraisonAIError (e.g. PraisonAIConfigError) always wins over the static per-class copy. Retryability is preserved from the source outcome — invalid_output maps to format_error but stays retryable=True when outcome.is_retryable() was True.

User Interaction Flow

A user asks the assistant on Slack to summarise a report. The configured OpenAI key has expired. Instead of Error: 401 Unauthorized, the assistant replies with a concrete next step, and the adapter records reason_code="auth_expired" for the operator.

Common Patterns

Offer a retry affordance only when it makes sense — inspect reply.retryable before showing a “Retry” button:
Aggregate by reason_code — because it’s a bounded, machine-readable enum, dashboards bucket failures by class (auth_expired, rate_limit, budget_exhausted, …) without regex on prose. Custom remediation_hint for domain errors — a PraisonAIConfigError that sets remediation_hint="Run the migration first: alembic upgrade head." delivers that hint verbatim, keyed by missing_key.

Best Practices

Route every failed turn through render_failure_reply so downgrade messaging stays consistent across channels.
The reason codes are stable module constants for exactly this reason — import REASON_RATE_LIMIT rather than typing "rate_limit".
When raising a custom error, set remediation_hint on the exception so the user sees the concrete next step rather than the generic per-class copy.
retryable mirrors the source outcome — do not re-derive retry policy from the text.

Visible-Outcome Guarantee

The empty-final counterpart — classify_final for blank turns

Intentional Silence

The deliberate no-reply path

Degraded Delivery

The presentation-path counterpart

Agent Run Outcomes

The input taxonomy render_failure_reply reads
Introduced in PraisonAI commit 892b9fb (fixes #3799).