From 81998e6309c4d898bc7f1b52244a1834dcc643b4 Mon Sep 17 00:00:00 2001 From: Jiayuan Zhang Date: Sat, 14 Feb 2026 02:04:17 +0800 Subject: [PATCH] fix(telegram): skip tool narration messages, only send final answer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the agent uses tools (web search, etc.), it generates intermediate narration text like "Let me search..." before each tool call. These were being sent as separate Telegram messages, causing message spam. Now we detect tool_use blocks in the message content and skip sending those intermediate messages — only the final answer reaches the user. Applied to both Desktop channel plugin and Gateway Telegram service. Co-Authored-By: Claude Opus 4.6 --- apps/gateway/telegram/telegram.service.ts | 11 ++++++++++- packages/core/src/agent/extract-text.ts | 8 ++++++++ packages/core/src/channels/manager.ts | 14 ++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/apps/gateway/telegram/telegram.service.ts b/apps/gateway/telegram/telegram.service.ts index 61147259..9f76df6a 100644 --- a/apps/gateway/telegram/telegram.service.ts +++ b/apps/gateway/telegram/telegram.service.ts @@ -1003,8 +1003,17 @@ export class TelegramService implements OnModuleInit, OnModuleDestroy { // Stop typing + send formatted text on message_end if (event.type === "message_end") { - this.stopTyping(deviceId); const agentMsg = (event as { message?: { content?: Array<{ type: string; text?: string }> } }).message; + + // Skip tool narration: if the message contains tool_use blocks, + // it's intermediate text (e.g. "Let me search...") before a tool call. + // Keep typing and wait for the final answer. + const hasToolUse = agentMsg?.content?.some((c) => c.type === "tool_use") ?? false; + if (hasToolUse) { + return; + } + + this.stopTyping(deviceId); if (agentMsg?.content) { const textContent = agentMsg.content .filter((c) => c.type === "text" && c.text) diff --git a/packages/core/src/agent/extract-text.ts b/packages/core/src/agent/extract-text.ts index 8075e61f..4a2e8afe 100644 --- a/packages/core/src/agent/extract-text.ts +++ b/packages/core/src/agent/extract-text.ts @@ -11,6 +11,14 @@ export function extractText(message: AgentMessage | undefined): string { .join(""); } +/** Check if an AgentMessage contains tool_use blocks (i.e., is a tool invocation, not a final answer) */ +export function hasToolUse(message: AgentMessage | undefined): boolean { + if (!message || typeof message !== "object" || !("content" in message)) return false; + const content = (message as { content?: Array<{ type: string }> }).content; + if (!Array.isArray(content)) return false; + return content.some((c) => c.type === "tool_use"); +} + /** Extract thinking/reasoning content from an AgentMessage */ export function extractThinking(message: AgentMessage | undefined): string { if (!message || typeof message !== "object" || !("content" in message)) return ""; diff --git a/packages/core/src/channels/manager.ts b/packages/core/src/channels/manager.ts index ad5af1b1..7e6009ff 100644 --- a/packages/core/src/channels/manager.ts +++ b/packages/core/src/channels/manager.ts @@ -23,6 +23,7 @@ import { listChannels } from "./registry.js"; import { loadChannelsConfig } from "./config.js"; import { MessageAggregator, DEFAULT_CHUNKER_CONFIG } from "../hub/message-aggregator.js"; import { isHeartbeatAckEvent } from "../hub/heartbeat-filter.js"; +import { hasToolUse } from "../agent/extract-text.js"; import type { AsyncAgent } from "../agent/async-agent.js"; import type { ChannelInfo } from "../agent/system-prompt/types.js"; import { transcribeAudio } from "../media/transcribe.js"; @@ -279,6 +280,19 @@ export class ChannelManager { this.createAggregator(); } + // Skip tool narration: if the assistant message contains tool_use blocks, + // it's intermediate narration (e.g. "Let me search...") before a tool call, + // not the final answer. Discard the buffered text instead of sending it. + if (event.type === "message_end" && role === "assistant") { + const message = (event as { message?: Parameters[0] }).message; + if (hasToolUse(message)) { + console.log("[Channels] Skipping tool narration message (has tool_use blocks)"); + this.aggregator?.reset(); + this.aggregator = null; + return; + } + } + if (this.aggregator) { this.aggregator.handleEvent(event); }