feat: channel-aware per-message directives via adapter hints (#419)
This commit is contained in:
@@ -312,6 +312,7 @@ Ask the bot owner to approve with:
|
||||
wasMentioned,
|
||||
isListeningMode,
|
||||
attachments,
|
||||
formatterHints: this.getFormatterHints(),
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -414,6 +415,14 @@ Ask the bot owner to approve with:
|
||||
return this.config.dmPolicy || 'pairing';
|
||||
}
|
||||
|
||||
getFormatterHints() {
|
||||
return {
|
||||
supportsReactions: true,
|
||||
supportsFiles: true,
|
||||
formatHint: 'Discord markdown: **bold** *italic* `code` [links](url) ```code blocks``` — supports headers',
|
||||
};
|
||||
}
|
||||
|
||||
supportsEditing(): boolean {
|
||||
return this.config.streaming ?? false;
|
||||
}
|
||||
@@ -477,6 +486,7 @@ Ask the bot owner to approve with:
|
||||
messageId: message.id,
|
||||
action,
|
||||
},
|
||||
formatterHints: this.getFormatterHints(),
|
||||
}).catch((err) => {
|
||||
log.error('Error handling reaction:', err);
|
||||
});
|
||||
|
||||
@@ -339,6 +339,14 @@ This code expires in 1 hour.`;
|
||||
return this.config.dmPolicy || 'pairing';
|
||||
}
|
||||
|
||||
getFormatterHints() {
|
||||
return {
|
||||
supportsReactions: true,
|
||||
supportsFiles: false,
|
||||
formatHint: 'ONLY: *bold* _italic_ `code` — NO: headers, code fences, links, quotes, tables',
|
||||
};
|
||||
}
|
||||
|
||||
supportsEditing(): boolean {
|
||||
return false;
|
||||
}
|
||||
@@ -868,6 +876,7 @@ This code expires in 1 hour.`;
|
||||
wasMentioned,
|
||||
isListeningMode,
|
||||
attachments: collectedAttachments.length > 0 ? collectedAttachments : undefined,
|
||||
formatterHints: this.getFormatterHints(),
|
||||
};
|
||||
|
||||
this.onMessage?.(msg).catch((err) => {
|
||||
|
||||
@@ -169,6 +169,7 @@ export class SlackAdapter implements ChannelAdapter {
|
||||
wasMentioned: false, // Regular messages; app_mention handles mentions
|
||||
isListeningMode: mode === 'listen',
|
||||
attachments,
|
||||
formatterHints: this.getFormatterHints(),
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -264,6 +265,7 @@ export class SlackAdapter implements ChannelAdapter {
|
||||
groupName: isGroup ? channelId : undefined,
|
||||
wasMentioned: true, // app_mention is always a mention
|
||||
attachments,
|
||||
formatterHints: this.getFormatterHints(),
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -359,6 +361,14 @@ export class SlackAdapter implements ChannelAdapter {
|
||||
return this.config.dmPolicy || 'pairing';
|
||||
}
|
||||
|
||||
getFormatterHints() {
|
||||
return {
|
||||
supportsReactions: true,
|
||||
supportsFiles: true,
|
||||
formatHint: 'Slack mrkdwn: *bold* _italic_ `code` <URL|text> — NO standard markdown headers',
|
||||
};
|
||||
}
|
||||
|
||||
/** Check if a channel is allowed by the groups config allowlist */
|
||||
private isChannelAllowed(channelId: string): boolean {
|
||||
return isGroupAllowed(this.config.groups, [channelId]);
|
||||
@@ -413,6 +423,7 @@ export class SlackAdapter implements ChannelAdapter {
|
||||
messageId,
|
||||
action,
|
||||
},
|
||||
formatterHints: this.getFormatterHints(),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -496,6 +496,7 @@ Reply **approve** or **deny** to this message.`;
|
||||
text,
|
||||
messageId,
|
||||
timestamp: new Date(message.date * 1000),
|
||||
formatterHints: this.getFormatterHints(),
|
||||
};
|
||||
|
||||
// Call handler
|
||||
@@ -749,6 +750,14 @@ Reply **approve** or **deny** to this message.`;
|
||||
return false;
|
||||
}
|
||||
|
||||
getFormatterHints() {
|
||||
return {
|
||||
supportsReactions: false,
|
||||
supportsFiles: false,
|
||||
formatHint: 'MarkdownV2: *bold* _italic_ `code` [link](url) — NO: headers, tables',
|
||||
};
|
||||
}
|
||||
|
||||
async sendMessage(msg: OutboundMessage): Promise<{ messageId: string }> {
|
||||
if (!this.client) {
|
||||
throw new Error('Client not initialized');
|
||||
|
||||
@@ -304,6 +304,7 @@ export class TelegramAdapter implements ChannelAdapter {
|
||||
groupName,
|
||||
wasMentioned,
|
||||
isListeningMode,
|
||||
formatterHints: this.getFormatterHints(),
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -349,6 +350,7 @@ export class TelegramAdapter implements ChannelAdapter {
|
||||
messageId: String(messageId),
|
||||
action,
|
||||
},
|
||||
formatterHints: this.getFormatterHints(),
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -409,6 +411,7 @@ export class TelegramAdapter implements ChannelAdapter {
|
||||
groupName,
|
||||
wasMentioned,
|
||||
isListeningMode,
|
||||
formatterHints: this.getFormatterHints(),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -427,6 +430,7 @@ export class TelegramAdapter implements ChannelAdapter {
|
||||
groupName,
|
||||
wasMentioned,
|
||||
isListeningMode,
|
||||
formatterHints: this.getFormatterHints(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -461,6 +465,7 @@ export class TelegramAdapter implements ChannelAdapter {
|
||||
wasMentioned,
|
||||
isListeningMode,
|
||||
attachments,
|
||||
formatterHints: this.getFormatterHints(),
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -633,6 +638,14 @@ export class TelegramAdapter implements ChannelAdapter {
|
||||
return this.config.dmPolicy || 'pairing';
|
||||
}
|
||||
|
||||
getFormatterHints() {
|
||||
return {
|
||||
supportsReactions: true,
|
||||
supportsFiles: true,
|
||||
formatHint: 'MarkdownV2: *bold* _italic_ `code` [link](url) — NO: headers, tables',
|
||||
};
|
||||
}
|
||||
|
||||
async sendTypingIndicator(chatId: string): Promise<void> {
|
||||
await this.bot.api.sendChatAction(chatId, 'typing');
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Each channel (Telegram, Slack, Discord, WhatsApp, Signal) implements this interface.
|
||||
*/
|
||||
|
||||
import type { ChannelId, InboundMessage, OutboundMessage, OutboundFile } from '../core/types.js';
|
||||
import type { ChannelId, InboundMessage, OutboundMessage, OutboundFile, FormatterHints } from '../core/types.js';
|
||||
|
||||
/**
|
||||
* Channel adapter - implement this for each messaging platform
|
||||
@@ -29,6 +29,7 @@ export interface ChannelAdapter {
|
||||
sendFile?(file: OutboundFile): Promise<{ messageId: string }>;
|
||||
addReaction?(chatId: string, messageId: string, emoji: string): Promise<void>;
|
||||
getDmPolicy?(): string;
|
||||
getFormatterHints(): FormatterHints;
|
||||
|
||||
// Event handlers (set by bot core)
|
||||
onMessage?: (msg: InboundMessage) => Promise<void>;
|
||||
|
||||
@@ -836,6 +836,7 @@ export class WhatsAppAdapter implements ChannelAdapter {
|
||||
isListeningMode,
|
||||
replyToUser: extracted.replyContext?.senderE164,
|
||||
attachments: extracted.attachments,
|
||||
formatterHints: this.getFormatterHints(),
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1002,6 +1003,14 @@ export class WhatsAppAdapter implements ChannelAdapter {
|
||||
return this.config.dmPolicy || 'pairing';
|
||||
}
|
||||
|
||||
getFormatterHints() {
|
||||
return {
|
||||
supportsReactions: false,
|
||||
supportsFiles: true,
|
||||
formatHint: 'WhatsApp: *bold* _italic_ `code` — NO: headers, code fences, links, tables',
|
||||
};
|
||||
}
|
||||
|
||||
supportsEditing(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -181,38 +181,67 @@ describe('formatMessageEnvelope', () => {
|
||||
expect(result).toContain('**Mentioned**: yes');
|
||||
});
|
||||
|
||||
it('does not include per-message directive hints (covered by system prompt)', () => {
|
||||
const groupMsg = createMessage({ isGroup: true });
|
||||
const dmMsg = createMessage({ isGroup: false });
|
||||
expect(formatMessageEnvelope(groupMsg)).not.toContain('Response Directives');
|
||||
expect(formatMessageEnvelope(dmMsg)).not.toContain('Response Directives');
|
||||
it('includes <actions> directives when reactions are supported', () => {
|
||||
const msg = createMessage({
|
||||
isGroup: false,
|
||||
formatterHints: { supportsReactions: true },
|
||||
});
|
||||
const result = formatMessageEnvelope(msg);
|
||||
expect(result).toContain('Response Directives');
|
||||
expect(result).toContain('<no-reply/>');
|
||||
expect(result).toContain('<actions>');
|
||||
});
|
||||
|
||||
it('omits <actions> directives when reactions are not supported', () => {
|
||||
const msg = createMessage({ isGroup: false });
|
||||
const result = formatMessageEnvelope(msg);
|
||||
expect(result).toContain('Response Directives');
|
||||
expect(result).toContain('<no-reply/>');
|
||||
expect(result).not.toContain('<react');
|
||||
});
|
||||
|
||||
it('shows file directive only when files supported', () => {
|
||||
const msg = createMessage({
|
||||
formatterHints: { supportsFiles: true },
|
||||
});
|
||||
const result = formatMessageEnvelope(msg);
|
||||
expect(result).toContain('<send-file');
|
||||
});
|
||||
|
||||
it('omits file directive when files not supported', () => {
|
||||
const msg = createMessage({
|
||||
formatterHints: { supportsFiles: false },
|
||||
});
|
||||
const result = formatMessageEnvelope(msg);
|
||||
expect(result).not.toContain('<send-file');
|
||||
});
|
||||
|
||||
it('shows minimal directives in listening mode', () => {
|
||||
const msg = createMessage({
|
||||
isGroup: true,
|
||||
isListeningMode: true,
|
||||
formatterHints: { supportsReactions: true },
|
||||
});
|
||||
const result = formatMessageEnvelope(msg);
|
||||
expect(result).toContain('<no-reply/>');
|
||||
expect(result).toContain('react to show you saw this');
|
||||
expect(result).not.toContain('react and reply');
|
||||
});
|
||||
});
|
||||
|
||||
describe('format hints', () => {
|
||||
it('includes Slack format hint', () => {
|
||||
const msg = createMessage({ channel: 'slack' });
|
||||
it('includes format hint when provided via formatterHints', () => {
|
||||
const msg = createMessage({
|
||||
formatterHints: { formatHint: 'MarkdownV2: *bold* _italic_' },
|
||||
});
|
||||
const result = formatMessageEnvelope(msg);
|
||||
expect(result).toContain('**Format support**: Markdown (auto-converted to Slack mrkdwn):');
|
||||
expect(result).toContain('**Format support**: MarkdownV2: *bold* _italic_');
|
||||
});
|
||||
|
||||
it('includes Telegram format hint', () => {
|
||||
const msg = createMessage({ channel: 'telegram' });
|
||||
it('omits Format support line when no formatHint is set', () => {
|
||||
const msg = createMessage({});
|
||||
const result = formatMessageEnvelope(msg);
|
||||
expect(result).toContain('**Format support**: MarkdownV2:');
|
||||
});
|
||||
|
||||
it('includes WhatsApp format hint', () => {
|
||||
const msg = createMessage({ channel: 'whatsapp' });
|
||||
const result = formatMessageEnvelope(msg);
|
||||
expect(result).toContain('**Format support**:');
|
||||
expect(result).toContain('NO: headers');
|
||||
});
|
||||
|
||||
it('includes Signal format hint', () => {
|
||||
const msg = createMessage({ channel: 'signal' });
|
||||
const result = formatMessageEnvelope(msg);
|
||||
expect(result).toContain('**Format support**: ONLY:');
|
||||
expect(result).not.toContain('**Format support**');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -13,17 +13,7 @@ export const SYSTEM_REMINDER_TAG = 'system-reminder';
|
||||
export const SYSTEM_REMINDER_OPEN = `<${SYSTEM_REMINDER_TAG}>`;
|
||||
export const SYSTEM_REMINDER_CLOSE = `</${SYSTEM_REMINDER_TAG}>`;
|
||||
|
||||
/**
|
||||
* Channel format hints - tells the agent what formatting syntax to use
|
||||
* Each channel has different markdown support - hints help agent format appropriately.
|
||||
*/
|
||||
const CHANNEL_FORMATS: Record<string, string> = {
|
||||
slack: 'Markdown (auto-converted to Slack mrkdwn): **bold** _italic_ `code` [links](url) ```code blocks``` - NO: headers, tables',
|
||||
discord: '**bold** *italic* `code` [links](url) ```code blocks``` - NO: headers, tables',
|
||||
telegram: 'MarkdownV2: *bold* _italic_ `code` [links](url) - NO: headers, tables',
|
||||
whatsapp: '*bold* _italic_ `code` - NO: headers, code fences, links, tables',
|
||||
signal: 'ONLY: *bold* _italic_ `code` - NO: headers, code fences, links, quotes, tables',
|
||||
};
|
||||
// Channel format hints are now provided per-message via formatterHints on InboundMessage.
|
||||
|
||||
export interface EnvelopeOptions {
|
||||
timezone?: 'local' | 'utc' | string; // IANA timezone or 'local'/'utc'
|
||||
@@ -229,7 +219,7 @@ function buildMetadataLines(msg: InboundMessage, options: EnvelopeOptions): stri
|
||||
lines.push(`- **Timestamp**: ${formatTimestamp(msg.timestamp, options)}`);
|
||||
|
||||
// Format support hint
|
||||
const formatHint = CHANNEL_FORMATS[msg.channel];
|
||||
const formatHint = msg.formatterHints?.formatHint;
|
||||
if (formatHint) {
|
||||
lines.push(`- **Format support**: ${formatHint}`);
|
||||
}
|
||||
@@ -256,7 +246,13 @@ function buildChatContextLines(msg: InboundMessage, options: EnvelopeOptions): s
|
||||
if (msg.wasMentioned) {
|
||||
lines.push(`- **Mentioned**: yes`);
|
||||
}
|
||||
lines.push(`- **Hint**: Use \`<no-reply/>\` to skip replying, \`<actions>\` for reactions/voice`);
|
||||
if (msg.isListeningMode) {
|
||||
lines.push(`- **Mode**: Listen only — observe and update memories, do not send text replies`);
|
||||
} else if (msg.formatterHints?.supportsReactions) {
|
||||
lines.push(`- **Hint**: See Response Directives below for \`<no-reply/>\` and \`<actions>\``);
|
||||
} else {
|
||||
lines.push(`- **Hint**: See Response Directives below for \`<no-reply/>\``);
|
||||
}
|
||||
} else {
|
||||
lines.push(`- **Type**: Direct message`);
|
||||
}
|
||||
@@ -301,6 +297,56 @@ export function buildSessionContext(options: SessionContextOptions): string[] {
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build context-aware Response Directives based on channel capabilities and chat type.
|
||||
* In listening mode, shows minimal directives. In normal mode, shows the full set
|
||||
* filtered by what the channel actually supports.
|
||||
*/
|
||||
function buildResponseDirectives(msg: InboundMessage): string[] {
|
||||
const lines: string[] = [];
|
||||
const supportsReactions = msg.formatterHints?.supportsReactions ?? false;
|
||||
const supportsFiles = msg.formatterHints?.supportsFiles ?? false;
|
||||
const isGroup = !!msg.isGroup;
|
||||
const isListeningMode = msg.isListeningMode ?? false;
|
||||
|
||||
// Listening mode: minimal directives only
|
||||
if (isListeningMode) {
|
||||
lines.push(`- \`<no-reply/>\` — acknowledge without replying (recommended)`);
|
||||
if (supportsReactions) {
|
||||
lines.push(`- \`<actions><react emoji="eyes" /></actions>\` — react to show you saw this`);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
// no-reply
|
||||
if (isGroup) {
|
||||
lines.push(`- \`<no-reply/>\` — skip replying when the message isn't directed at you`);
|
||||
} else {
|
||||
lines.push(`- \`<no-reply/>\` — skip replying when the message doesn't need a response`);
|
||||
}
|
||||
|
||||
// actions/react (only if channel supports it)
|
||||
if (supportsReactions) {
|
||||
lines.push(`- \`<actions><react emoji="thumbsup" /></actions>\` — react without sending text (executes silently)`);
|
||||
lines.push(`- \`<actions><react emoji="eyes" /></actions>Your text here\` — react and reply`);
|
||||
if (isGroup) {
|
||||
lines.push(`- \`<actions><react emoji="fire" message="123" /></actions>\` — react to a specific message`);
|
||||
}
|
||||
lines.push(`- Emoji names: eyes, thumbsup, heart, fire, tada, clap — or unicode`);
|
||||
lines.push(`- Prefer directives over tool calls for reactions (faster and cheaper)`);
|
||||
}
|
||||
|
||||
// voice memo (always available -- TTS config is server-side)
|
||||
lines.push(`- \`<actions><voice>Your message here</voice></actions>\` — send a voice memo via TTS`);
|
||||
|
||||
// file sending (only if channel supports it)
|
||||
if (supportsFiles) {
|
||||
lines.push(`- \`<send-file path="/path/to/file.png" kind="image" />\` — send a file (restricted to configured directory)`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a message with XML system-reminder envelope.
|
||||
*
|
||||
@@ -351,6 +397,9 @@ export function formatMessageEnvelope(
|
||||
sections.push(`## Chat Context\n${contextLines.join('\n')}`);
|
||||
}
|
||||
|
||||
// Channel-aware response directives
|
||||
const directiveLines = buildResponseDirectives(msg);
|
||||
sections.push(`## Response Directives\n${directiveLines.join('\n')}`);
|
||||
// Build the full system-reminder block
|
||||
const reminderContent = sections.join('\n\n');
|
||||
const reminder = `${SYSTEM_REMINDER_OPEN}\n${reminderContent}\n${SYSTEM_REMINDER_CLOSE}`;
|
||||
@@ -418,8 +467,15 @@ export function formatGroupBatchEnvelope(
|
||||
});
|
||||
|
||||
// Format hint
|
||||
const formatHint = CHANNEL_FORMATS[first.channel];
|
||||
const formatHint = first.formatterHints?.formatHint;
|
||||
const hint = formatHint ? `\n(Format: ${formatHint})` : '';
|
||||
|
||||
return `${header}\n${lines.join('\n')}${hint}`;
|
||||
// Compact directives for batch
|
||||
const supportsReactions = first.formatterHints?.supportsReactions ?? false;
|
||||
const directiveParts = isListeningMode
|
||||
? [`\`<no-reply/>\` to acknowledge`, ...(supportsReactions ? [`\`<actions><react emoji="eyes" /></actions>\` to react`] : [])]
|
||||
: [`\`<no-reply/>\` to skip replying`, ...(supportsReactions ? [`\`<actions><react emoji="thumbsup" /></actions>\` to react`] : [])];
|
||||
const directives = `\n(Directives: ${directiveParts.join(', ')})`;
|
||||
|
||||
return `${header}\n${lines.join('\n')}${hint}${directives}`;
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ export class GroupBatcher {
|
||||
isListeningMode: messages.every((m) => m.isListeningMode === true) ? true : undefined,
|
||||
isBatch: true,
|
||||
batchedMessages: messages,
|
||||
formatterHints: last.formatterHints,
|
||||
};
|
||||
|
||||
this.onFlush(batchMsg, adapter);
|
||||
|
||||
@@ -35,6 +35,7 @@ describe('channel redaction wrapping', () => {
|
||||
sendMessage: sendSpy,
|
||||
editMessage: vi.fn(async () => {}),
|
||||
sendTypingIndicator: vi.fn(async () => {}),
|
||||
getFormatterHints: () => ({ supportsReactions: false, supportsFiles: false }),
|
||||
};
|
||||
|
||||
bot.registerChannel(adapter);
|
||||
|
||||
@@ -6,7 +6,18 @@ 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.
|
||||
Not every message requires a response. Before replying, consider whether your response adds value. When in doubt, prefer \`<no-reply/>\` over a low-value response.
|
||||
|
||||
## Choosing Not to Reply
|
||||
|
||||
Use \`<no-reply/>\` when the message:
|
||||
- Is a simple acknowledgment ("ok", "thanks", "got it") that doesn't need a follow-up
|
||||
- Is a conversation between other users that you're not part of
|
||||
- Is a notification or status update with no question or request
|
||||
- Has already been addressed in a previous turn
|
||||
- Is in a group chat and not directed at you
|
||||
|
||||
Channel-specific response options (reactions, file sending) are listed in the **Response Directives** section of each incoming message.
|
||||
|
||||
# Communication System
|
||||
|
||||
@@ -74,54 +85,6 @@ 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.
|
||||
|
||||
## Response Directives
|
||||
|
||||
You can include an \`<actions>\` block at the **start** of your response to perform actions alongside your reply. The entire block is stripped before your message is sent.
|
||||
|
||||
\`\`\`
|
||||
<actions>
|
||||
<react emoji="👍" />
|
||||
</actions>
|
||||
Great idea!
|
||||
\`\`\`
|
||||
|
||||
This sends "Great idea!" and reacts with thumbsup.
|
||||
|
||||
### Available directives
|
||||
|
||||
- \`<react emoji="👀" />\` -- react to the message you are responding to. Use the actual emoji character (👀, 👍, ❤️, 🔥, 🎉, 👏).
|
||||
- \`<react emoji="🔥" message="123" />\` -- react to a specific message by ID.
|
||||
- \`<send-file path="/path/to/file.png" kind="image" caption="..." />\` -- send a file or image to the same channel/chat. File paths are restricted to the configured send-file directory (default: \`data/outbound/\` in the working directory). Paths outside this directory are blocked.
|
||||
- \`<send-file path="/path/to/voice.ogg" kind="audio" cleanup="true" />\` -- send a voice note. Audio files (.ogg, .mp3, etc.) are sent as native voice memos on Telegram and WhatsApp. Use \`cleanup="true"\` to delete the file after sending.
|
||||
- \`<voice>Your message here</voice>\` -- generate and send a voice memo. The text is converted to speech via TTS and sent as a native voice note. No tool calls needed. Use for short conversational replies, responding to voice messages, or when the user asks for audio.
|
||||
|
||||
### Actions-only response
|
||||
|
||||
An \`<actions>\` block with no text after it executes silently (nothing sent to the user), like \`<no-reply/>\`:
|
||||
|
||||
\`\`\`
|
||||
<actions>
|
||||
<react emoji="👀" />
|
||||
</actions>
|
||||
\`\`\`
|
||||
|
||||
Prefer directives over tool calls for simple actions like reactions. They are faster and cheaper.
|
||||
|
||||
## Available Channels
|
||||
|
||||
- **telegram** - Telegram messenger
|
||||
|
||||
@@ -84,6 +84,16 @@ export interface InboundMessage {
|
||||
isBatch?: boolean; // Is this a batched group message?
|
||||
batchedMessages?: InboundMessage[]; // Original individual messages (for batch formatting)
|
||||
isListeningMode?: boolean; // Listening mode: agent processes for memory but response is suppressed
|
||||
formatterHints?: FormatterHints; // Channel capabilities for directive rendering
|
||||
}
|
||||
|
||||
/**
|
||||
* Channel capability hints for per-message directive rendering
|
||||
*/
|
||||
export interface FormatterHints {
|
||||
supportsReactions?: boolean;
|
||||
supportsFiles?: boolean;
|
||||
formatHint?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,6 +56,10 @@ export class MockChannelAdapter implements ChannelAdapter {
|
||||
return false; // Disable streaming edits for simpler testing
|
||||
}
|
||||
|
||||
getFormatterHints() {
|
||||
return { supportsReactions: false, supportsFiles: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate an inbound message and wait for response
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user