Skip to main content
Add return_outcome=True to agent.run() or agent.astart() and the call returns a small frozen RunOutcome telling you exactly how the run ended — instead of raising and making you catch every exception type.
RunOutcome (this page) and AgentRunOutcome (see Agent Run Outcomes) are two different types with two different jobs:

Quick Start

1

Simple usage (bool)

Branch on outcome.succeeded — no try/except.
2

Async with a hard timeout

astart() accepts a run-level timeout (seconds). When it elapses, reason is hard_timeout.
3

Full gateway/bot dispatch

The headline use case — one branch per terminal reason, no exception chains.

How It Works

A host receives a request, dispatches it to the agent with return_outcome=True, and picks a channel action from the single reason field. The five terminal reasons and the action each one signals: Precedence is sticky: a hard_timeout is never silently downgraded by a later cleanup error.

Which reason applies when

A nested inner asyncio.TimeoutError (a tool or handoff that exhausted its own budget) resolves to failed, not hard_timeout. Only the run-level timeout budget produces hard_timeout.

Configuration Options

RunOutcome is a frozen dataclass. Every field: Class methods: from_exception matches by type name: hardtimeout/runtimeout/budgettimeouthard_timeout; asyncio.CancelledError or supersed/interrupt/cancelledcancelled; abort/drain/shutdownaborted; everything else → failed with error=str(exc). TerminalReason is a Literal["completed", "hard_timeout", "cancelled", "aborted", "failed"] (with a str fallback on Python <3.8), exported from praisonaiagents.agent. New kwargs on run() and astart(): Verified behaviour:
  • outcome.reason == "hard_timeout" is authoritative and sticky — never downgraded.
  • A nested inner asyncio.TimeoutError resolves to failed, not hard_timeout.
  • External asyncio.CancelledError on the enclosing task is re-raised — host shutdown is honoured, not swallowed.
  • RunOutcome is frozen — assigning to .reason raises FrozenInstanceError.

Common Patterns

Simple success/failure branch:
Retry ladder — retry only on failed, escalate on hard_timeout, drop the rest:
Manual normalisation in a wrapper that owns its own try/except:

Best Practices

A hard_timeout is the run-level budget — the run didn’t finish. A failed carrying a nested inner asyncio.TimeoutError is a tool/handoff timeout inside the run; only the second one is often safely retryable.
A try/except around astart() without return_outcome=True can absorb host-shutdown cancellation. Use return_outcome=True and check reason instead — external asyncio.CancelledError is re-raised, not hidden.
The point of RunOutcome is to remove isinstance chains from your gateway. If you’re still catching exceptions, you’ve lost the benefit — branch on outcome.reason.
It’s a frozen dataclass. Build a new one with RunOutcome.completed(...) or RunOutcome.from_exception(...) instead of mutating fields.

Migration

The default behaviour of run() / astart() is unchanged — they still return a str and raise on error, so existing code needs no change. Opt in with one flag to collapse three catches into one branch.

Agent Run Outcomes

The different AgentRunOutcome type — a typed status for validation and handoff results, not run-level dispatch.

Autonomy Loop

The autonomy loop’s own completion_reason — conceptually adjacent, but a separate mechanism.