Skip to main content
Structured classification of why an outbound send failed, so the retry loop stops burning budget on permanent failures. Every gateway/bot adapter now tags a failed send with a SendErrorKind. Permanent failures — a blocked user, a deleted chat, a revoked token — short-circuit instead of exhausting the retry budget against a dead target.

Quick Start

1

Benefit automatically

Any adapter that inherits BasePlatformAdapter gains status-code-aware short-circuiting with no code change. A permanent failure returns immediately instead of retrying max_retries times.
2

Inspect the result

After a delivery, read error_kind and retryable to react — rotate credentials on auth_fatal, mark a target dead on forbidden.

How It Works

Failures are classified once at the boundary via classify_error(exc). Only retryable kinds are retried; provably-permanent kinds short-circuit immediately, and the router records the dead target by kind. The retry loop (BasePlatformAdapter._send_with_retry) stamps chat_id onto the classified SendResult, so the caller can act on result.error_kind without re-parsing the error string.

Which Kind Gets Returned?

The classifier keys off the HTTP/platform status code first, then falls back to a small set of cross-platform substrings when no status is present. asyncio.TimeoutError, TimeoutError, ConnectionError, and OSError are treated as transient before the text fallback runs.

SendErrorKind Reference

Seven members, spelled exactly as in praisonaiagents/bots/base.py. The enum is a str-Enum: SendErrorKind.FORBIDDEN.value == "forbidden".
SendResult.retryable defaults to True. Setting error_kind alone does not stop retries unless you also set retryable=False (or use classify_send_error, which derives it for you).

Writing a Custom Adapter

Override classify_error to map your platform SDK’s native exceptions into the taxonomy with full fidelity. Fall through to classify_send_error for the shared status/text-aware default.
The default classify_error delegates to classify_send_error, so an adapter that skips this override still short-circuits standard HTTP failures.

Serialization & Observability

SendResult.to_dict() exposes error_kind as the enum value (a plain string) plus the derived retryable flag — ready for log pipelines and dashboards.
error_kind is None on success. Record dead targets by kind so dashboards read forbidden: bot was kicked instead of an opaque str(exc).

Best Practices

The default’s text fallback is deliberately narrow so it stays cross-platform. Your SDK’s typed exceptions give perfect fidelity — map them directly and fall through to classify_send_error for the rest.
A blocked user or deleted chat cannot be recovered by retry. Short-circuiting is a feature, not a bug — keep retryable=False for forbidden, target_not_found, auth_fatal, and invalid_request.
Record the failure by kind (e.g. reason="forbidden: bot was kicked"), matching what DeliveryRouter now does. Machine-readable kinds make dashboards and alerts readable.
unknown preserves the historical retry-everything behaviour and prevents silent drops of failures the classifier can’t name. Do not special-case it into a permanent failure.
This is a fully backward-compatible change. Existing YAML, existing Python Agent(...) code, and existing custom adapters that don’t override classify_error all keep working — they simply gain automatic status-code-aware short-circuiting via the default implementation.
The unknown → forbidden upgrade only happens in the bot layer (_resilience.classify_send_failure), where the broader is_permanent_target_failure predicate recognises confirmed whole-target deaths. Core classify_send_error never upgrades a kind.

Durable Outbound Delivery

Retry-with-backoff and dead-letter queue mechanics

Gateway Channel Supervision

Dead-target parking and degraded credential states