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); }