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.

Diagram

Visible-Outcome Guarantee

silence

empty

text

πŸ€– Agent Final

πŸ” classify_final

🀫 NO_REPLY β€” suppress

πŸ’¬ empty_final_message

βœ… Deliver as-is

πŸ“€ Adapter

Quick Start

  1. 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.",
    )
  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
    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.

Diagram
Slack / Telegram / Discord / …classify_finalfire_message_sending()AgentSlack / Telegram / Discord / …classify_finalfire_message_sending()Agentalt[silence (NO_REPLY / [SILENT])][empty (blank / whitespace / [tool_calls: …])][text]final replyclassify_final(content)“silence”suppress send (deliberate)“empty”substitute empty_final_messagedeliver fallback sentence“text”deliver as-is

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.

OutcomeWhat the user sees
Visible replyThe agent’s real message, delivered as-is
FallbackThe empty_final_message sentence (empty/placeholder final)
Deliberate silenceNothing β€” 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
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"
ReturnMeaningTrigger
"silence"Deliberate no-reply β€” send is suppressed on purposeExact NO_REPLY or [SILENT] marker
"empty"Blank / whitespace / raw [tool_calls: …] placeholder β€” fallback substitutedEverything not covered by the other two
"text"Real user-facing content β€” delivered as-isNon-empty content that is not a silence token

Configuration Options

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

OptionTypeDefaultWhere to setDescription
empty_final_messagestr"Task completed β€” no message to show."BotConfig.empty_final_message or BotConfig.metadata["empty_final_message"] or YAML metadata.empty_final_messageSentence delivered when an inbound turn produces an empty/placeholder final that is not intentional silence.
Direct attribute
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.")
Metadata seam
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."})
bot.yaml
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