Summary
PraisonAI's bot gateway already gates inbound messages well (group_policy: respond_all | mention_only | command_only), but once a message passes that gate the agent's reply is always delivered. There is no first-class, cross-platform way for the agent itself to look at a message and decide "this does not need a response — stay silent".
This is the missing half of world-class group-chat behaviour. Today an operator is forced into a binary:
respond_all — the bot replies to every message in the group (spammy, quickly muted/removed), or
mention_only — the bot can only react when explicitly @-mentioned, so it can never proactively help (e.g. answer a question nobody thought to direct at it).
A genuinely useful "ambient" assistant reads everything but speaks selectively — it jumps in when it can add value and otherwise says nothing. That requires the agent to be able to return an intentional-silence signal that the gateway honours by suppressing delivery, uniformly across every channel. PraisonAI has no such signal.
Current behaviour
Inbound gating exists and is good, but it is static and platform-side — it decides whether to run the agent, not whether the agent's answer is worth sending:
# src/praisonai/praisonai/bots/_config_schema.py:149
group_policy: str = "mention_only" # respond_all | mention_only | command_only
# src/praisonai/praisonai/bots/discord.py:270-274
should_respond = False
if isinstance(message.channel, discord.DMChannel):
should_respond = True
elif self._client.user.mentioned_in(message) or not self.config.mention_required:
should_respond = True
Once the agent runs, its reply flows unconditionally to the platform. Every adapter shares the same send path via fire_message_sending (Telegram/Slack/Discord/WhatsApp/email/agentmail all call it):
# src/praisonai/praisonai/bots/telegram.py:403-406
send_result = self.fire_message_sending(
str(update.message.chat_id), str(response),
reply_to=str(update.message.message_id),
)
if send_result["cancel"]:
... # only cancels if an external MESSAGE_SENDING hook returns cancel
# src/praisonai/praisonai/bots/_protocol_mixin.py:158-202
def fire_message_sending(self, channel_id, content, reply_to=None) -> Dict[str, Any]:
"""Hooks can modify content or cancel the send."""
result = {"content": content, "cancel": False}
...
if runner.is_blocked(hook_results):
result["cancel"] = True
...
So cancellation is possible only if the operator writes and wires a custom MESSAGE_SENDING lifecycle hook that inspects content and returns a block decision. That is:
- not built-in / not safe-by-default,
- generic content cancellation by an external hook — not the agent self-determining silence as part of its own reasoning,
- bespoke per deployment, with no canonical, prompt-discoverable contract telling the model how to signal "no reply".
Confirmed absent:
grep -rniE "intentional_silence|NO_REPLY|\[SILENT\]" src/praisonai-agents/praisonaiagents src/praisonai/praisonai/bots → no matches (no canonical silence token, no predicate, no system-prompt seed).
- No bot module suppresses delivery based on an agent-emitted control token; a blank/empty reply is also not treated as deliberate silence (it falls on the empty/error path, not a silence path).
Desired behaviour
A first-class, opt-in intentional silence capability:
- A single canonical, documented control token (e.g.
NO_REPLY) the agent may emit as its entire response to mean "deliberately send nothing".
- The gateway recognises the token in the shared delivery path so every adapter suppresses delivery identically (no placeholder left behind, no error surfaced, run still recorded in session/mirror so the agent keeps context of what it chose to skip).
- Strict matching: a substantive reply that merely mentions
NO_REPLY is delivered normally; only an exact-token reply is treated as silence. A blank reply is not silence (that stays on the empty/error path).
- When enabled for a channel, the agent's system prompt is automatically seeded with the contract ("If a group message needs no response from you, reply with exactly
NO_REPLY"), so the behaviour is discoverable without the user hand-writing prompt text.
This unlocks group_policy: respond_all as a genuinely usable mode: the agent sees every message but only speaks when useful.
Layer placement
- Primary layer: wrapper (
src/praisonai/praisonai/bots/) — recognise the silence token in the shared fire_message_sending / delivery path and suppress send, so all adapters honour it with one change; add the per-channel toggle and prompt seed.
- Why not core: core stays protocol/contract-only; the actual delivery suppression is bot transport behaviour. (Core gains only a tiny dependency-free helper — see secondary touch.)
- Why not tools: silence is not an agent-callable capability invoked mid-task; it is a property of the final reply on the delivery lifecycle.
- Why not plugins: this is intrinsic, safe-by-default gateway behaviour every group deployment needs, not an optional cross-cutting policy. (A guardrail plugin could approximate it via a
MESSAGE_SENDING hook, but that is exactly today's bespoke, non-default situation this issue removes.)
- Secondary touch (core): add a pure, zero-dependency helper + constant in
praisonaiagents (e.g. praisonaiagents.bots.silence: SILENT_REPLY_TOKEN and is_intentional_silence_response(text) -> bool) so the contract lives once in core and the wrapper + any consumer reuse it.
- 3-way surface (CLI + YAML + Python): yes —
gateway.yaml channels.<name>.allow_silence: true (+ optional custom token), CLI praisonai bot telegram --allow-silence, Python Bot("telegram", agent=agent, allow_silence=True).
Proposed approach
- Extension point: core helper/constant (contract) + wrapper recognition in the shared send path + per-channel config flag + automatic system-prompt seed.
- Minimal API sketch:
# core: praisonaiagents/bots/silence.py (pure, no heavy imports)
SILENT_REPLY_TOKEN = "NO_REPLY"
_MARKERS = frozenset({"NO_REPLY", "[SILENT]", "SILENT"})
def is_intentional_silence_response(text: str | None) -> bool:
"""True only when the reply is *exactly* a silence marker (not prose mentioning it)."""
if not text:
return False # blank != deliberate silence
return text.strip().strip("[]").upper() in _MARKERS
# wrapper: src/praisonai/praisonai/bots/_protocol_mixin.py (shared by all adapters)
def fire_message_sending(self, channel_id, content, reply_to=None):
if getattr(self, "_allow_silence", False) and is_intentional_silence_response(content):
return {"content": "", "cancel": True, "silent": True} # suppress uniformly
... # existing hook logic unchanged
Resolution sketch
# Before (today): only two options, neither is "smart selective"
channels:
telegram:
token: ${TELEGRAM_BOT_TOKEN}
group_policy: respond_all # replies to EVERY message (spam)
# or mention_only # can NEVER help proactively
# After (proposed): ambient assistant — reads all, speaks when useful
channels:
telegram:
token: ${TELEGRAM_BOT_TOKEN}
group_policy: respond_all
allow_silence: true # agent may emit NO_REPLY to stay quiet
# Agent reasoning, unchanged API — the gateway honours the choice:
# user (group): "lol same" -> agent replies "NO_REPLY" -> nothing sent
# user (group): "how do I reset it?" -> agent answers the question -> delivered
Severity
High — selective participation is the defining behaviour of a group/ambient chat assistant. Without it, respond_all is unusable in real groups and mention_only cannot proactively help; the only workaround is bespoke per-deployment hook code. The fix is small (one shared delivery-path change + a dependency-free core helper + a config flag) and the value to gateway ease-of-use and "feels world-class in a group" is large.
Validation
src/praisonai/praisonai/bots/_config_schema.py:149,202-208 — group_policy gates inbound only (respond_all/mention_only/command_only); no output-side silence concept.
src/praisonai/praisonai/bots/discord.py:270-274 (and equivalents) — should_respond is static inbound gating, not agent-decided.
src/praisonai/praisonai/bots/_protocol_mixin.py:158-202 — the shared fire_message_sending can only cancel via an external MESSAGE_SENDING hook returning a block decision; there is no built-in agent-emitted silence token.
src/praisonai/praisonai/bots/telegram.py:403-415 — confirms the reply is delivered unless an external hook cancels; a blank reply is treated as empty/error, not deliberate silence.
grep -rniE "intentional_silence|NO_REPLY|\[SILENT\]" src/praisonai-agents src/praisonai/praisonai/bots → no matches: no canonical token, predicate, or prompt seed exists in core or wrapper.
- All seven adapters (
telegram/slack/discord/whatsapp/email/agentmail + mixin) route through fire_message_sending, so a single shared change covers every channel.
Summary
PraisonAI's bot gateway already gates inbound messages well (
group_policy:respond_all|mention_only|command_only), but once a message passes that gate the agent's reply is always delivered. There is no first-class, cross-platform way for the agent itself to look at a message and decide "this does not need a response — stay silent".This is the missing half of world-class group-chat behaviour. Today an operator is forced into a binary:
respond_all— the bot replies to every message in the group (spammy, quickly muted/removed), ormention_only— the bot can only react when explicitly @-mentioned, so it can never proactively help (e.g. answer a question nobody thought to direct at it).A genuinely useful "ambient" assistant reads everything but speaks selectively — it jumps in when it can add value and otherwise says nothing. That requires the agent to be able to return an intentional-silence signal that the gateway honours by suppressing delivery, uniformly across every channel. PraisonAI has no such signal.
Current behaviour
Inbound gating exists and is good, but it is static and platform-side — it decides whether to run the agent, not whether the agent's answer is worth sending:
Once the agent runs, its reply flows unconditionally to the platform. Every adapter shares the same send path via
fire_message_sending(Telegram/Slack/Discord/WhatsApp/email/agentmail all call it):So cancellation is possible only if the operator writes and wires a custom
MESSAGE_SENDINGlifecycle hook that inspects content and returns a block decision. That is:Confirmed absent:
grep -rniE "intentional_silence|NO_REPLY|\[SILENT\]" src/praisonai-agents/praisonaiagents src/praisonai/praisonai/bots→ no matches (no canonical silence token, no predicate, no system-prompt seed).Desired behaviour
A first-class, opt-in intentional silence capability:
NO_REPLY) the agent may emit as its entire response to mean "deliberately send nothing".NO_REPLYis delivered normally; only an exact-token reply is treated as silence. A blank reply is not silence (that stays on the empty/error path).NO_REPLY"), so the behaviour is discoverable without the user hand-writing prompt text.This unlocks
group_policy: respond_allas a genuinely usable mode: the agent sees every message but only speaks when useful.Layer placement
src/praisonai/praisonai/bots/) — recognise the silence token in the sharedfire_message_sending/ delivery path and suppress send, so all adapters honour it with one change; add the per-channel toggle and prompt seed.MESSAGE_SENDINGhook, but that is exactly today's bespoke, non-default situation this issue removes.)praisonaiagents(e.g.praisonaiagents.bots.silence:SILENT_REPLY_TOKENandis_intentional_silence_response(text) -> bool) so the contract lives once in core and the wrapper + any consumer reuse it.gateway.yamlchannels.<name>.allow_silence: true(+ optional custom token), CLIpraisonai bot telegram --allow-silence, PythonBot("telegram", agent=agent, allow_silence=True).Proposed approach
Resolution sketch
Severity
High — selective participation is the defining behaviour of a group/ambient chat assistant. Without it,
respond_allis unusable in real groups andmention_onlycannot proactively help; the only workaround is bespoke per-deployment hook code. The fix is small (one shared delivery-path change + a dependency-free core helper + a config flag) and the value to gateway ease-of-use and "feels world-class in a group" is large.Validation
src/praisonai/praisonai/bots/_config_schema.py:149,202-208—group_policygates inbound only (respond_all/mention_only/command_only); no output-side silence concept.src/praisonai/praisonai/bots/discord.py:270-274(and equivalents) —should_respondis static inbound gating, not agent-decided.src/praisonai/praisonai/bots/_protocol_mixin.py:158-202— the sharedfire_message_sendingcan only cancel via an externalMESSAGE_SENDINGhook returning a block decision; there is no built-in agent-emitted silence token.src/praisonai/praisonai/bots/telegram.py:403-415— confirms the reply is delivered unless an external hook cancels; a blank reply is treated as empty/error, not deliberate silence.grep -rniE "intentional_silence|NO_REPLY|\[SILENT\]" src/praisonai-agents src/praisonai/praisonai/bots→ no matches: no canonical token, predicate, or prompt seed exists in core or wrapper.telegram/slack/discord/whatsapp/email/agentmail+ mixin) route throughfire_message_sending, so a single shared change covers every channel.