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.
Quick Start
- It's on by default
The guarantee is always active β you don’t enable it. A normal agent already benefits:
python from praisonaiagents import Agent agent = Agent( name="Support Bot", instructions="Answer the user's question.", ) - Customise the fallback sentence
Set
empty_final_messageinBotConfig.metadatato change the sentence shown when a turn produces no visible text:python 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."} )
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.
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.
| 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.
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. |
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.")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."})metadata:
empty_final_message: "I'm on it β no message this turn."Best Practices
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.
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. Returning whitespace will not silence the bot; it still substitutes the fallback.
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.
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
Deliberate NO_REPLY / [SILENT] β suppress the send on purpose
Where the visible-outcome guarantee is enforced
Inbound JournalCrash-replay β the other half of “no inbound ends without a recorded outcome”
Channel DescriptorDeclare per-channel config, prompt hints, and setup in one place
CardEvery failed turn ends in a visible, actionable reply keyed by failure class