> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Visible-Outcome Guarantee

> Every inbound turn ends in a visible reply or a deliberate, recorded silence — never a blank.

Every inbound message ends in one of two recorded states: a visible reply, or a deliberate silence — a blank or placeholder final is never dropped.

> [!NOTE]
>
> To make this guarantee hold across a crash in the **send window**, run `BotSessionManager(defer_journal_completion=True)`. It keeps the inbound entry pending until the reply is durably delivered, so a crash between agent-done and send re-drives the turn instead of settling the inbound with no visible outcome. See [Inbound Journal → Defer completion until reply is delivered](https://praison.ai/docs/features/inbound-journal.md#defer-completion-until-reply-is-delivered-opt-in).

> [!NOTE]
>
> For turns that **fail** (expired auth, missing key, rate limit, budget, doom-loop), [`render_failure_reply`](https://praison.ai/docs/features/failure-reply) is the failure-path counterpart of `classify_final` — it maps the failure to a visible, actionable reply.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    subgraph "Visible-Outcome Guarantee"
        A[🤖 Agent Final] --> B{🔍 classify_final}
        B -->|silence| S[🤫 NO_REPLY — suppress]
        B -->|empty| F[💬 empty_final_message]
        B -->|text| T[✅ Deliver as-is]
        F --> D[📤 Adapter]
        T --> D
    end

    classDef input fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef process fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef result fill:#10B981,stroke:#7C90A0,color:#fff
    classDef silence fill:#6366F1,stroke:#7C90A0,color:#fff

    class A input
    class B process
    class S silence
    class F,T,D result
```

## Quick Start

**1. It's on by default**

The guarantee is always active — you don't enable it. A normal agent already benefits:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(
    name="Support Bot",
    instructions="Answer the user's question.",
)
```

**2. Customise the fallback sentence**

Set `empty_final_message` in `BotConfig.metadata` to change the sentence shown when a turn produces no visible text:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonaiagents.bots import BotConfig

agent = Agent(
    name="Support Bot",
    instructions="Answer the user's question.",
)

config = BotConfig(
    metadata={"empty_final_message": "I'm on it — no message this turn."}
)
```

> [!NOTE]
>
> Before this guarantee, three real bugs could each end an inbound turn with no reply: **Slack** silently dropped a blank send, **Telegram** parked an empty send in the DLQ, and **any adapter** could leak a raw `[tool_calls: search_web]` placeholder to the user.

***

## How It Works

Classification and substitution happen once, at `fire_message_sending()` — the single delivery funnel every adapter shares — so no adapter re-decides "blank" differently.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Agent
    participant Funnel as fire_message_sending()
    participant Classify as classify_final
    participant Adapter as Slack / Telegram / Discord / …

    Agent->>Funnel: final reply
    Funnel->>Classify: classify_final(content)
    alt silence (NO_REPLY / [SILENT])
        Classify-->>Funnel: "silence"
        Funnel-->>Adapter: suppress send (deliberate)
    else empty (blank / whitespace / [tool_calls: …])
        Classify-->>Funnel: "empty"
        Funnel->>Funnel: substitute empty_final_message
        Funnel-->>Adapter: deliver fallback sentence
    else text
        Classify-->>Funnel: "text"
        Funnel-->>Adapter: deliver as-is
    end
```

A blank, whitespace-only, or `[tool_calls: …]` final is treated as **empty, not silence** and substituted with the fallback. A blank reply is never confused with a deliberate `NO_REPLY`.

> [!NOTE]
>
> On the channel `send_message` contract, the typed [`SendResult.status`](https://praison.ai/docs/features/bot-platform-adapter#branching-on-delivery-outcome) (`"sent" | "failed" | "queued" | "duplicate"`) is the machine-checkable expression of this guarantee — every send resolves to one of four named outcomes, so a silent drop is unrepresentable rather than merely discouraged.

| Outcome | What the user sees |
| - | - |
| Visible reply | The agent's real message, delivered as-is |
| Fallback | The `empty_final_message` sentence (empty/placeholder final) |
| Deliberate silence | Nothing — `NO_REPLY` / `[SILENT]` suppresses the send on purpose |

***

## classify\_final

`classify_final` is the single decision the bot layer uses to tell a real reply, an empty final, and a deliberate silence apart.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.bots import classify_final

classify_final("NO_REPLY")                  # "silence"
classify_final("")                          # "empty"
classify_final("   ")                       # "empty"
classify_final("[tool_calls: search_web]")  # "empty"
classify_final("Here is your answer.")      # "text"
```

| Return | Meaning | Trigger |
| - | - | - |
| `"silence"` | Deliberate no-reply — send is suppressed on purpose | Exact `NO_REPLY` or `[SILENT]` marker |
| `"empty"` | Blank / whitespace / raw `[tool_calls: …]` placeholder — fallback substituted | Everything not covered by the other two |
| `"text"` | Real user-facing content — delivered as-is | Non-empty content that is not a silence token |

***

## Configuration Options

The fallback sentence is the only knob. Precedence: direct attribute > metadata > default.

| Option | Type | Default | Where to set | Description |
| - | - | - | - | - |
| `empty_final_message` | `str` | `"Task completed — no message to show."` | `BotConfig.empty_final_message` **or** `BotConfig.metadata["empty_final_message"]` **or** YAML `metadata.empty_final_message` | Sentence delivered when an inbound turn produces an empty/placeholder final that is *not* intentional silence. |

```python Direct attribute theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.bots import BotConfig

# Programmatic callers can set the attribute directly
config = BotConfig(empty_final_message="I'm on it — no message this turn.")
```

```python Metadata seam theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents.bots import BotConfig

# YAML-friendly seam — no new typed knob needed
config = BotConfig(metadata={"empty_final_message": "I'm on it — no message this turn."})
```

```yaml bot.yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
metadata:
  empty_final_message: "I'm on it — no message this turn."
```

> [!NOTE]
>
> **Operator visibility.** Every substitution is logged at `INFO` on the wrapper logger with the platform name:
>
> ```
> Empty-final resolution: substituted fallback for a blank/placeholder reply on <platform> (visible-outcome guarantee)
> ```
>
> Repeated hits usually mean an agent prompt or tool graph is finishing without composing a reply.

***

## Best Practices

> [accordion] Keep the fallback short and platform-agnostic
>
> The same sentence may ship on Slack, Telegram, Discord, WhatsApp, email, or AgentMail. Avoid platform-specific formatting or references.

> [accordion] Use NO\_REPLY when you want silence
>
> To make the bot say nothing on purpose, return the exact `NO_REPLY` / `[SILENT]` token — see [Intentional Silence](https://praison.ai/docs/features/bot-intentional-silence.md). Returning whitespace will **not** silence the bot; it still substitutes the fallback.

> [accordion] Watch the INFO log for repeated substitutions
>
> Frequent `"Empty-final resolution: substituted fallback…"` lines signal an agent finishing without a reply. Treat it as a prompt/tool-graph smell, not a delivery problem.

> [accordion] Prefer the metadata form in bot.yaml
>
> `metadata.empty_final_message` is the declared seam for platform-agnostic extras and needs no code change — set it in YAML rather than adding a typed knob.

***

## Related

> [card] [Intentional Silence](https://praison.ai/docs/features/bot-intentional-silence.md)
>
> Deliberate `NO_REPLY` / `[SILENT]` — suppress the send on purpose

> [card] [Bot Gateway](https://praison.ai/docs/features/bot-gateway.md)
>
> Where the visible-outcome guarantee is enforced

> [card] [Inbound Journal](https://praison.ai/docs/features/inbound-journal.md)
>
> Crash-replay — the other half of "no inbound ends without a recorded outcome"

> [card] [Channel Descriptor](https://praison.ai/docs/features/channel-descriptor.md)
>
> Declare per-channel config, prompt hints, and setup in one place

> [card]
>
> Every failed turn ends in a visible, actionable reply keyed by failure class
>
> [Open](https://praison.ai/docs/features/failure-reply.md)

> [!NOTE]
>
> Introduced in [PraisonAI PR #3622](https://github.com/MervinPraison/PraisonAI/pull/3622) (fixes [#3621](https://github.com/MervinPraison/PraisonAI/issues/3621)).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.