From b1d69965b527a04827f8fc79b1e5ffc8262bc439 Mon Sep 17 00:00:00 2001 From: Cameron Date: Fri, 6 Feb 2026 11:08:56 -0800 Subject: [PATCH] feat: add silent marker for agent opt-out (#196) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add {{NO_REPLY}} silent marker for agent opt-out Allow the agent to respond with {{NO_REPLY}} to suppress message delivery for messages that don't warrant a reply. The marker is checked in three places: the streaming edit guard (prefix match to prevent partial sends), finalizeMessage(), and the post-stream response handler. Co-Authored-By: Claude Opus 4.5 * refactor: use XML marker instead of {{NO_REPLY}} Switch to XML-style self-closing tag for consistency with the XML envelope format used elsewhere, and because LLMs produce well-formed XML tags more reliably than template syntax. Co-Authored-By: Claude Opus 4.6 * feat: add no-reply hint to group chat envelopes Agents created outside lettabot (via ADE, Letta Cloud) won't have the system prompt telling them about . Adding the hint to group chat envelopes makes the opt-out mechanism self-documenting. Written by Cameron ◯ Letta Code "Silence is one of the great arts of conversation." -- Marcus Tullius Cicero --------- Co-authored-by: Gabriele Sarti Co-authored-by: Claude Opus 4.5 --- package-lock.json | 1 + src/core/bot.ts | 20 +++++++++++++++++++- src/core/formatter.test.ts | 12 ++++++++++++ src/core/formatter.ts | 1 + src/core/system-prompt.ts | 17 +++++++++++++++++ 5 files changed, 50 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 798b4f0..4b4957b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "lettabot", "version": "1.0.0", + "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { "@clack/prompts": "^0.11.0", diff --git a/src/core/bot.ts b/src/core/bot.ts index 7f914fb..92d9903 100644 --- a/src/core/bot.ts +++ b/src/core/bot.ts @@ -443,6 +443,15 @@ export class LettaBot { // Helper to finalize and send current accumulated response const finalizeMessage = async () => { + // Check for silent marker - agent chose not to reply + if (response.trim() === '') { + console.log('[Bot] Agent chose not to reply (no-reply marker)'); + sentAnyMessage = true; + response = ''; + messageId = null; + lastUpdate = Date.now(); + return; + } if (response.trim()) { try { if (messageId) { @@ -521,8 +530,10 @@ export class LettaBot { response += streamMsg.content; // Stream updates only for channels that support editing (Telegram, Slack) + // Hold back streaming edits while response could still become const canEdit = adapter.supportsEditing?.() ?? true; - if (canEdit && Date.now() - lastUpdate > 500 && response.length > 0) { + const mayBeNoReply = ''.startsWith(response.trim()); + if (canEdit && !mayBeNoReply && Date.now() - lastUpdate > 500 && response.length > 0) { try { if (messageId) { await adapter.editMessage(msg.chatId, messageId, response); @@ -600,6 +611,13 @@ export class LettaBot { clearInterval(typingInterval); } + // Check for silent marker - agent chose not to reply + if (response.trim() === '') { + console.log('[Bot] Agent chose not to reply (no-reply marker)'); + sentAnyMessage = true; + response = ''; + } + // Send final response if (response.trim()) { try { diff --git a/src/core/formatter.test.ts b/src/core/formatter.test.ts index 21bdbab..793aea2 100644 --- a/src/core/formatter.test.ts +++ b/src/core/formatter.test.ts @@ -180,6 +180,18 @@ describe('formatMessageEnvelope', () => { const result = formatMessageEnvelope(msg); expect(result).toContain('**Mentioned**: yes'); }); + + it('includes no-reply hint for group chats', () => { + const msg = createMessage({ isGroup: true }); + const result = formatMessageEnvelope(msg); + expect(result).toContain(''); + }); + + it('omits no-reply hint for DMs', () => { + const msg = createMessage({ isGroup: false }); + const result = formatMessageEnvelope(msg); + expect(result).not.toContain('no-reply'); + }); }); describe('format hints', () => { diff --git a/src/core/formatter.ts b/src/core/formatter.ts index 1f110cb..2cdaf45 100644 --- a/src/core/formatter.ts +++ b/src/core/formatter.ts @@ -231,6 +231,7 @@ function buildChatContextLines(msg: InboundMessage, options: EnvelopeOptions): s if (msg.wasMentioned) { lines.push(`- **Mentioned**: yes`); } + lines.push(`- **Hint**: To skip replying, respond with exactly: \`\``); } else { lines.push(`- **Type**: Direct message`); } diff --git a/src/core/system-prompt.ts b/src/core/system-prompt.ts index f064232..c71883f 100644 --- a/src/core/system-prompt.ts +++ b/src/core/system-prompt.ts @@ -6,6 +6,8 @@ export const SYSTEM_PROMPT = `You are a self-improving AI agent with advanced me You are connected to a multi-channel messaging system (LettaBot) that allows you to communicate with users across Telegram, Slack, Discord, WhatsApp, and Signal. You run on a remote server and can execute tools, manage files, and interact with various services. +Not every message requires a response. Before replying, consider whether your response adds value. In group chats especially, avoid replying to messages not directed at you, simple acknowledgments, or conversations between other users. Quality over quantity — only reply when you have something meaningful to contribute. + # Communication System You communicate through multiple channels and trigger types. Understanding when your messages are delivered is critical: @@ -68,6 +70,21 @@ During heartbeats and background tasks: You don't need to notify the user about everything. Use judgment about what's worth interrupting them for. +## Choosing Not to Reply + +Not all messages warrant a response. If a message doesn't need a reply, respond with exactly: + +\`\` + +This suppresses the message so nothing is sent to the user. Use this for: +- Messages in a group not directed at you +- Simple acknowledgments (e.g., "ok", "thanks", thumbs up) +- Conversations between other users you don't need to join +- Notifications or updates that don't require a response +- Messages you've already addressed + +When in doubt, prefer \`\` over a low-value response. Users appreciate an agent that knows when to stay quiet. + ## Available Channels - **telegram** - Telegram messenger