Agent Run Outcomes

Typed, exhaustively-matchable results for agent runs, validation, and handoffs

Run outcomes give every agent execution a typed status so your code can handle success, failure, timeout, cancellation, and invalid output exhaustively — no string matching.

Python

python
from praisonaiagents import Agent, AgentRunOutcome

agent = Agent(name="Research Agent", instructions="Research and report clearly")
outcome = agent.start("Summarise agent run outcomes")
assert isinstance(outcome, AgentRunOutcome)

TypeScript

typescript
import { AgentRunOutcome } from 'praisonai';

const outcome = AgentRunOutcome.success('Summarised agent run outcomes');
console.log(outcome.status);      // 'success'
console.log(outcome.isSuccess()); // true

const failed = AgentRunOutcome.failure('validation failed');
console.log(failed.status);       // 'failure'

The user handles every finish state in code; the agent returns a typed AgentRunOutcome instead of an ambiguous string.

Diagram

Agent Run Outcomes

🤖 Agent Run

Status?

✅ success

❌ failure

⏰ timeout

🛑 cancelled

🚫 invalid_output

Quick Start

  1. Simple Usage

    Every agent execution returns an AgentRunOutcome that you can check for success.

    python
    from praisonaiagents import Agent, AgentRunOutcome
    
    agent = Agent(
        name="Research Agent",
        instructions="Research the given topic thoroughly"
    )
    
    # Agent executes and produces an outcome
    result = agent.start("Research renewable energy")
    
    # Check the outcome
    if result.outcome.is_success():
        print(f"Success: {result.outcome.output}")
    else:
        print(f"Failed: {result.outcome.error}")
  2. Exhaustive Matching

    Match all five status types exhaustively to handle every possible outcome.

    python
    from praisonaiagents import Agent, AgentRunOutcome
    
    agent = Agent(name="Validator")
    result = agent.start("Validate this data")
    
    # Handle all possible outcomes
    if result.outcome.status == "success":
        process_success(result.outcome.output)
    elif result.outcome.status == "timeout":
        retry_with_longer_timeout(result)
    elif result.outcome.status == "invalid_output":
        fix_validation_and_retry(result)
    elif result.outcome.status == "cancelled":
        handle_cancellation(result)
    elif result.outcome.status == "failure":
        log_permanent_error(result.outcome.error)
  3. Validation Routing

    Access typed outcomes from task validation instead of parsing strings.

    python
    from praisonaiagents import Agent, Task, PraisonAIAgents
    
    # Setup workflow with validation
    workflow = PraisonAIAgents(
        agents=[validator_agent, executor_agent],
        tasks=[
            Task(description="Validate input", task_type="decision"),
            Task(description="Execute if valid")
        ]
    )
    
    result = workflow.kickoff()
    
    # Check validation outcome
    validation_task = result.tasks[0]
    if validation_task.validation_outcome.status == "invalid_output":
        print(f"Validation failed: {validation_task.validation_outcome.error}")
        retry_with_fixes()
    else:
        proceed_to_execution()

How It Works

Diagram
TaskValidatorAgentUserTaskValidatorAgentUserRequest executionValidate outputAgentRunOutcomeStore validation_outcomeTyped result

Every agent execution produces an AgentRunOutcome with a typed status that enables exhaustive pattern matching:

StatusMeaningRetryableWhen It Occurs
successCompleted successfullyNoAgent produced valid output
failureNon-retryable logic errorNoUnhandled exception, logic error
timeoutOperation exceeded time limitYesSlow LLM, network hang
cancelledExternal cancel signalNoUser Ctrl-C, parent cancelled child
invalid_outputOutput didn’t pass validationYesWrong format, validator rejected

The Five Statuses

StatusMeaningRetryableTypical Cause
successCompleted successfullyNoAgent produced valid output
failureNon-retryable logic errorNoUnhandled exception, validation logic error
timeoutOperation exceeded time limitYesSlow LLM, slow tool, network hang
cancelledExternal cancel signalNoUser Ctrl-C, parent agent cancelled child
invalid_outputOutput didn’t pass validationYesWrong format, missing fields, validator rejected

Creating Outcomes

Create outcomes using factory methods — each sets appropriate defaults automatically.

python
from praisonaiagents import AgentRunOutcome

# Success with output
outcome = AgentRunOutcome.success(
    output="Task completed successfully",
    elapsed_s=2.5,
    agent_name="Research Agent"
)

# General failure
outcome = AgentRunOutcome.failure(
    error="Unexpected error occurred",
    error_category="execution",
    agent_name="Analysis Agent"
)

# Timeout - auto-sets error_category="timeout"
outcome = AgentRunOutcome.timeout(
    error="Agent timed out after 30s",
    elapsed_s=30.0
)

# Cancelled - auto-sets error_category="cancelled"
outcome = AgentRunOutcome.cancelled(
    error="User cancelled the operation"
)

# Invalid output - auto-sets error_category="validation"
outcome = AgentRunOutcome.invalid_output(
    error="Output format is invalid"
)

Retry Decisions

Use is_retryable() to determine if an operation can be retried safely.

python
from praisonaiagents import Agent

agent = Agent(name="Processor")

for attempt in range(3):
    result = agent.start("Process data")

    if result.outcome.is_success():
        break
    elif result.outcome.is_retryable():
        print(f"Attempt {attempt + 1} failed: {result.outcome.error}")
        continue
    else:
        print(f"Permanent failure: {result.outcome.error}")
        break

Reading the Outcome From a Task

After task validation routing, check both the new typed outcome and legacy feedback.

python
from praisonaiagents import Task, PraisonAIAgents

# Task with decision routing
validation_task = Task(
    description="Validate the user input",
    task_type="decision"
)

workflow = PraisonAIAgents(
    agents=[validator_agent],
    tasks=[validation_task]
)

result = workflow.kickoff()

# Access typed outcome (recommended)
outcome = validation_task.validation_outcome
if outcome.status == "invalid_output":
    print(f"Validation failed: {outcome.error}")

# Legacy dict still available for backward compatibility
feedback = validation_task.validation_feedback
print(f"Legacy status: {feedback['status']}")

HandoffResult Integration

Handoff results now include typed outcomes automatically derived from legacy fields.

python
from praisonaiagents import Agent, handoff

billing_agent = Agent(name="Billing Agent")
support_agent = Agent(
    name="Support Agent",
    handoffs=[billing_agent]
)

# Handoff to billing agent
result = support_agent.handoff_to(billing_agent, "Handle billing issue")

# Check typed outcome
if result.outcome.status == "timeout":
    print(f"Handoff timed out: {result.outcome.error}")
    retry_handoff()
elif result.outcome.is_success():
    print(f"Handoff successful: {result.outcome.output}")

Migration from String-Based Validation

Replace string parsing with typed status matching for safer code.

Before:

python
# String parsing - error-prone
if decision_str in ["invalid", "retry", "failed", "errors"]:
    handle_validation_failure()
elif decision_str in ["success", "valid", "approved"]:
    proceed_with_task()

After:

python
# Typed matching - exhaustive and safe
if outcome.status == "invalid_output":
    handle_validation_failure()
elif outcome.status == "success":
    proceed_with_task()

For gradual migration, use the compatibility helper:

python
from praisonaiagents import validate_decision_string

# Convert legacy string to typed status
status = validate_decision_string(legacy_decision_str)
if status == "invalid_output":
    handle_invalid_output()

Process normalises its routing key with .lower().strip() — the same normalisation validate_decision_string applies at praisonaiagents/run_outcome.py:303. Before the fix in MervinPraison/PraisonAI#4260, the two disagreed on a whitespace-padded verdict like "invalid\n"; now they agree, so the typed outcome is reliably stored even when the model pads its verdict.


Decision Flow

Diagram

Task completed

Operation timed out

User cancelled

Validation failed

Logic error

Which factory method?

What happened?

AgentRunOutcome.success

AgentRunOutcome.timeout

AgentRunOutcome.cancelled

AgentRunOutcome.invalid_output

AgentRunOutcome.failure

Typed outcome created


Best Practices

Prefer status matching over string parsing

Always match against the typed status field instead of parsing error messages or legacy strings.

python
# Good - exhaustive type-safe matching
if outcome.status == "invalid_output":
    retry_with_fixes()

# Bad - brittle string parsing
if "invalid" in str(outcome.error):
    retry_with_fixes()
Use `is_retryable()` to drive retry logic

Let the outcome determine retry behavior instead of hardcoding status lists.

python
# Good - use built-in retry detection
if outcome.is_retryable():
    schedule_retry()

# Bad - maintain your own retry logic
if outcome.status in ["timeout", "invalid_output"]:
    schedule_retry()
Put structured data in `context`, not in `error`

Use the context field for machine-readable metadata, keep error human-readable.

python
# Good - structured context
outcome = AgentRunOutcome.failure(
    error="Database connection failed",
    context={
        "database_host": "db.example.com",
        "timeout_seconds": 30,
        "retry_count": 3
    }
)

# Bad - structured data in error message
outcome = AgentRunOutcome.failure(
    error="Database connection failed: host=db.example.com, timeout=30s"
)
Always handle all five statuses

Exhaustive matching prevents bugs from unhandled cases. Avoid fall-through else clauses.

python
# Good - explicit handling
if outcome.status == "success":
    handle_success(outcome.output)
elif outcome.status == "timeout":
    handle_timeout()
elif outcome.status == "invalid_output":
    handle_validation_error()
elif outcome.status == "cancelled":
    handle_cancellation()
elif outcome.status == "failure":
    handle_permanent_error()
# No else clause needed - all cases covered

# Bad - fall-through else
if outcome.status == "success":
    handle_success()
else:
    # What exactly happened? Unknown!
    handle_generic_error()

CLI Mapping (praisonai run)

The praisonai run CLI wraps the SDK outcome into a success / truncated / failure exit signal for pipelines: any falsy or empty result (None, "", all-whitespace) exits 1 with a machine-readable run_failed payload, unless the core recorded a provider block/refusal/truncation — in which case the specific reason wins and exits 2 with the reason as status. A non-empty result that hit the step limit exits 2 with a truncated payload; any other non-empty result exits 0 with the existing text/JSON output.

SDK outcomeCLI exit code--output json status
success (non-empty output, agent.last_stop_reason != "max_steps")0absent (success payload)
success (non-empty output, agent.last_stop_reason == "max_steps") — a step-limit-truncated finalisation summary2"truncated" (result preserved as the summary text)
content_filtered (agent.last_stop_reason == "content_filtered")2"content_filtered" (result preserves any partial text)
refused (agent.last_stop_reason == "refused")2"refused" (result preserves any partial text)
length_truncated (agent.last_stop_reason == "length_truncated")2"length_truncated" (result preserves any partial text). Surfaced only after the auto-continuation cap is exhausted — see Output Continuation.
failure / timeout / cancelled / invalid_output (falsy or empty result surfaced to Agent.start / run)1"failed" (with code: "run_failed")

agent.last_stop_reason is one of "completed", "max_steps", "content_filtered", "refused", "length_truncated", "timeout", or "error". "timeout" means the turn was cut by ExecutionConfig.max_execution_time or a run_autonomous(timeout_seconds=...) deadline (see Execution Time Caps); the CLI has no distinct exit code for it, so a non-empty partial result exits 0.

See praisonai run → Exit Codes for the full CLI contract, the failure and truncated payload schemas, and CI usage examples.


Related