fix(telegram): skip tool narration messages, only send final answer

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 <noreply@anthropic.com>
This commit is contained in:
Jiayuan Zhang 2026-02-14 02:04:17 +08:00
parent 8270762d66
commit 81998e6309
3 changed files with 32 additions and 1 deletions

View file

@ -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)

View file

@ -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 "";

View file

@ -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<typeof hasToolUse>[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);
}