/stop, a provider client — and RunTerminal folds their racing signals into one authoritative outcome that only ever refines toward stronger attribution.
RunTerminal is a third, lower-level primitive — distinct from RunOutcome (host-facing terminal for a single run) and AgentRunOutcome (typed validation/handoff status). Reach for it in gateway/ledger scenarios where several observers watch the same run and you need one sticky, order-independent outcome.Quick Start
1
Simplest merge (no current)
With no recorded outcome, the observation becomes authoritative.
2
Deliberate cancel is never downgraded
A user’s
/stop wins over a late provider error, whatever the arrival order.3
Round-trip through a durable ledger
Persist the outcome typed, not as a free string — then rehydrate it.
How It Works
Each observer reports aRunTerminal to merge_run_terminal, which keeps refining one authoritative outcome; collapse projects it to a RunStatus for downstream consumers.
Precedence & Stickiness
merge_run_terminal picks a winner by two rules: a stronger kind always beats a weaker one, and among equal kinds a sticky source beats a non-sticky one.
Kind precedence ladder — a later, weaker observation never overwrites a stronger one.
Sticky sources — a deliberate cancel, a hard run-budget timeout, and a supersede are intentional terminals; once recorded, authoritative.
Configuration Options
RunTerminal is a frozen dataclass. Every field:
Methods:
The
source vocabulary and which sources are sticky:
Full API surface lives in the auto-generated SDK reference for
praisonaiagents.run_outcome.
Which source to pick
Pick thesource that names the observer that ended the run.
Common Patterns
Gateway race — an idle timeout arrives, then the user cancels; the cancel wins:Best Practices
Always merge, never assign
Always merge, never assign
Replacing
record.terminal with the last observation is the exact bug this contract fixes. Always call merge_run_terminal(record.terminal, observed) so a late provider error can never overwrite a deliberate cancel or a hard timeout.Persist typed via to_dict
Persist typed via to_dict
Store
outcome.to_dict() in your durable ledger and rehydrate with RunTerminal.from_dict(...). The free-string form is the ambiguity this contract removes — keep the outcome typed end to end.Collapse only at the boundary
Collapse only at the boundary
Keep
RunTerminal internally and call collapse() only when you must emit a legacy RunStatus — a chat reaction, an exit code, an HTTP status. Collapsing early throws away the source attribution.Use is_sticky to stop watching
Use is_sticky to stop watching
Once a terminal is sticky, its outcome cannot change. Check
is_sticky(outcome) and stop watching the run — further observations can only be discarded anyway.Related
Run Outcome
The host-facing
RunOutcome for a single run — related, but a different primitive with a reason field.Agent Run Outcomes
The typed
AgentRunOutcome status for validation and handoff results — related, but a different primitive.
