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 withreturn_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 innerasyncio.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/budgettimeout → hard_timeout; asyncio.CancelledError or supersed/interrupt/cancelled → cancelled; abort/drain/shutdown → aborted; 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.TimeoutErrorresolves tofailed, nothard_timeout. - External
asyncio.CancelledErroron the enclosing task is re-raised — host shutdown is honoured, not swallowed. RunOutcomeis frozen — assigning to.reasonraisesFrozenInstanceError.
Common Patterns
Simple success/failure branch:failed, escalate on hard_timeout, drop the rest:
Best Practices
Never assume timeout means retry
Never assume timeout means retry
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.Don't swallow cancelled
Don't swallow cancelled
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.Route on reason, not on exception identity
Route on reason, not on exception identity
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.Treat RunOutcome as immutable
Treat RunOutcome as immutable
It’s a frozen dataclass. Build a new one with
RunOutcome.completed(...) or RunOutcome.from_exception(...) instead of mutating fields.Migration
The default behaviour ofrun() / 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.
Related
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.
