feat: add <no-reply/> silent marker for agent opt-out (#196)
* 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 <noreply@anthropic.com>
* refactor: use <no-reply/> 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 <noreply@anthropic.com>
* 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 <no-reply/>. 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 <gabriele.sarti996@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@@ -7,6 +7,7 @@
|
||||
"": {
|
||||
"name": "lettabot",
|
||||
"version": "1.0.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@clack/prompts": "^0.11.0",
|
||||
|
||||
@@ -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() === '<no-reply/>') {
|
||||
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 <no-reply/>
|
||||
const canEdit = adapter.supportsEditing?.() ?? true;
|
||||
if (canEdit && Date.now() - lastUpdate > 500 && response.length > 0) {
|
||||
const mayBeNoReply = '<no-reply/>'.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() === '<no-reply/>') {
|
||||
console.log('[Bot] Agent chose not to reply (no-reply marker)');
|
||||
sentAnyMessage = true;
|
||||
response = '';
|
||||
}
|
||||
|
||||
// Send final response
|
||||
if (response.trim()) {
|
||||
try {
|
||||
|
||||
@@ -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('<no-reply/>');
|
||||
});
|
||||
|
||||
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', () => {
|
||||
|
||||
@@ -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: \`<no-reply/>\``);
|
||||
} else {
|
||||
lines.push(`- **Type**: Direct message`);
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
\`<no-reply/>\`
|
||||
|
||||
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 \`<no-reply/>\` over a low-value response. Users appreciate an agent that knows when to stay quiet.
|
||||
|
||||
## Available Channels
|
||||
|
||||
- **telegram** - Telegram messenger
|
||||
|
||||
Reference in New Issue
Block a user