diff --git a/docs/configuration.md b/docs/configuration.md index 007a347..51199aa 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -65,17 +65,20 @@ channels: enabled: true token: "123456:ABC-DEF..." dmPolicy: pairing + # streaming: true # Opt-in: progressively edit messages as tokens arrive slack: enabled: true botToken: xoxb-... appToken: xapp-... dmPolicy: pairing + # streaming: true discord: enabled: true token: "..." dmPolicy: pairing + # streaming: true whatsapp: enabled: true @@ -368,6 +371,7 @@ All channels share these common options: | `instantGroups` | string[] | Group/channel IDs that bypass debounce entirely (legacy) | | `groups` | object | Per-group configuration map (use `*` as default) | | `mentionPatterns` | string[] | Extra regex patterns for mention detection (Telegram/WhatsApp/Signal) | +| `streaming` | boolean | Stream responses via progressive message edits (default: false; Telegram/Discord/Slack only) | ### Group Message Debouncing diff --git a/src/channels/discord.ts b/src/channels/discord.ts index 10bb4d5..a6ad40a 100644 --- a/src/channels/discord.ts +++ b/src/channels/discord.ts @@ -26,6 +26,7 @@ export interface DiscordConfig { token: string; dmPolicy?: DmPolicy; // 'pairing' (default), 'allowlist', or 'open' allowedUsers?: string[]; // Discord user IDs + streaming?: boolean; // Stream responses via progressive message edits (default: false) attachmentsDir?: string; attachmentsMaxBytes?: number; groups?: Record; // Per-guild/channel settings @@ -414,7 +415,7 @@ Ask the bot owner to approve with: } supportsEditing(): boolean { - return true; + return this.config.streaming ?? false; } private async handleReactionEvent( diff --git a/src/channels/slack.ts b/src/channels/slack.ts index 083d02a..4ab77d6 100644 --- a/src/channels/slack.ts +++ b/src/channels/slack.ts @@ -24,6 +24,7 @@ export interface SlackConfig { appToken: string; // xapp-... (for Socket Mode) dmPolicy?: 'pairing' | 'allowlist' | 'open'; allowedUsers?: string[]; // Slack user IDs (e.g., U01234567) + streaming?: boolean; // Stream responses via progressive message edits (default: false) attachmentsDir?: string; attachmentsMaxBytes?: number; groups?: Record; // Per-channel settings @@ -326,6 +327,10 @@ export class SlackAdapter implements ChannelAdapter { return { messageId: ts }; } + supportsEditing(): boolean { + return this.config.streaming ?? false; + } + async editMessage(chatId: string, messageId: string, text: string): Promise { if (!this.app) throw new Error('Slack not started'); diff --git a/src/channels/telegram.ts b/src/channels/telegram.ts index c9fae73..218526e 100644 --- a/src/channels/telegram.ts +++ b/src/channels/telegram.ts @@ -27,6 +27,7 @@ export interface TelegramConfig { token: string; dmPolicy?: DmPolicy; // 'pairing' (default), 'allowlist', or 'open' allowedUsers?: number[]; // Telegram user IDs (config allowlist) + streaming?: boolean; // Stream responses via progressive message edits (default: false) attachmentsDir?: string; attachmentsMaxBytes?: number; mentionPatterns?: string[]; // Regex patterns for mention detection @@ -600,6 +601,10 @@ export class TelegramAdapter implements ChannelAdapter { return { messageId: String(result.message_id) }; } + supportsEditing(): boolean { + return this.config.streaming ?? false; + } + async editMessage(chatId: string, messageId: string, text: string): Promise { const { markdownToTelegramV2 } = await import('./telegram-format.js'); try { diff --git a/src/config/types.ts b/src/config/types.ts index 9a8fe53..a5290c7 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -273,6 +273,7 @@ export interface TelegramConfig { token?: string; dmPolicy?: 'pairing' | 'allowlist' | 'open'; allowedUsers?: string[]; + streaming?: boolean; // Stream responses via progressive message edits (default: false) groupDebounceSec?: number; // Debounce interval in seconds (default: 5, 0 = immediate) groupPollIntervalMin?: number; // @deprecated Use groupDebounceSec instead instantGroups?: string[]; // Group chat IDs that bypass batching @@ -299,6 +300,7 @@ export interface SlackConfig { botToken?: string; dmPolicy?: 'pairing' | 'allowlist' | 'open'; allowedUsers?: string[]; + streaming?: boolean; // Stream responses via progressive message edits (default: false) groupDebounceSec?: number; // Debounce interval in seconds (default: 5, 0 = immediate) groupPollIntervalMin?: number; // @deprecated Use groupDebounceSec instead instantGroups?: string[]; // Channel IDs that bypass batching @@ -345,6 +347,7 @@ export interface DiscordConfig { token?: string; dmPolicy?: 'pairing' | 'allowlist' | 'open'; allowedUsers?: string[]; + streaming?: boolean; // Stream responses via progressive message edits (default: false) groupDebounceSec?: number; // Debounce interval in seconds (default: 5, 0 = immediate) groupPollIntervalMin?: number; // @deprecated Use groupDebounceSec instead instantGroups?: string[]; // Guild/server IDs or channel IDs that bypass batching diff --git a/src/core/bot.ts b/src/core/bot.ts index 77999b5..b945663 100644 --- a/src/core/bot.ts +++ b/src/core/bot.ts @@ -2082,7 +2082,7 @@ export class LettaBot implements AgentSession { // Live-edit streaming for channels that support it // Hold back streaming edits while response could still be or block - const canEdit = adapter.supportsEditing?.() ?? true; + const canEdit = adapter.supportsEditing?.() ?? false; const trimmed = response.trim(); const mayBeHidden = ''.startsWith(trimmed) || ''.startsWith(trimmed) diff --git a/src/main.ts b/src/main.ts index 7bf0448..27509ab 100644 --- a/src/main.ts +++ b/src/main.ts @@ -365,6 +365,7 @@ function createChannelsForAgent( allowedUsers: agentConfig.channels.telegram!.allowedUsers && agentConfig.channels.telegram!.allowedUsers.length > 0 ? agentConfig.channels.telegram!.allowedUsers.map(u => typeof u === 'string' ? parseInt(u, 10) : u) : undefined, + streaming: agentConfig.channels.telegram!.streaming, attachmentsDir, attachmentsMaxBytes, groups: agentConfig.channels.telegram!.groups, @@ -396,6 +397,7 @@ function createChannelsForAgent( allowedUsers: agentConfig.channels.slack.allowedUsers && agentConfig.channels.slack.allowedUsers.length > 0 ? agentConfig.channels.slack.allowedUsers : undefined, + streaming: agentConfig.channels.slack.streaming, attachmentsDir, attachmentsMaxBytes, groups: agentConfig.channels.slack.groups, @@ -452,6 +454,7 @@ function createChannelsForAgent( allowedUsers: agentConfig.channels.discord.allowedUsers && agentConfig.channels.discord.allowedUsers.length > 0 ? agentConfig.channels.discord.allowedUsers : undefined, + streaming: agentConfig.channels.discord.streaming, attachmentsDir, attachmentsMaxBytes, groups: agentConfig.channels.discord.groups,