diff --git a/pkg/agent/adapters/channelmanager.go b/pkg/agent/adapters/channelmanager.go index 8265ef99d..ad0840e86 100644 --- a/pkg/agent/adapters/channelmanager.go +++ b/pkg/agent/adapters/channelmanager.go @@ -43,3 +43,9 @@ func (a *channelManagerAdapter) SendMedia(ctx context.Context, msg bus.OutboundM func (a *channelManagerAdapter) SendPlaceholder(ctx context.Context, channel, chatID string) bool { return a.inner.SendPlaceholder(ctx, channel, chatID) } + +func (a *channelManagerAdapter) DismissToolFeedback( + ctx context.Context, channel, chatID string, outboundCtx *bus.InboundContext, +) { + a.inner.DismissToolFeedback(ctx, channel, chatID, outboundCtx) +} diff --git a/pkg/agent/interfaces/interfaces.go b/pkg/agent/interfaces/interfaces.go index bdf483e20..2efec05e1 100644 --- a/pkg/agent/interfaces/interfaces.go +++ b/pkg/agent/interfaces/interfaces.go @@ -44,4 +44,11 @@ type ChannelManager interface { // SendPlaceholder sends a placeholder message (e.g., for audio transcription). SendPlaceholder(ctx context.Context, channel, chatID string) bool + + // DismissToolFeedback clears any tracked tool feedback animation for the + // given channel/chat. Call this when a turn ends without a final response + // (e.g., ResponseHandled tools) to avoid orphaned animation goroutines. + // outboundCtx carries topic/thread info needed for channels that use + // scoped tracker keys (e.g., Telegram forum topics); may be nil. + DismissToolFeedback(ctx context.Context, channel, chatID string, outboundCtx *bus.InboundContext) } diff --git a/pkg/agent/pipeline_execute.go b/pkg/agent/pipeline_execute.go index 9935f2c9e..f6a8eaad6 100644 --- a/pkg/agent/pipeline_execute.go +++ b/pkg/agent/pipeline_execute.go @@ -704,6 +704,9 @@ toolLoop: } ts.setPhase(TurnPhaseCompleted) ts.setFinalContent("") + if al.channelManager != nil && ts.channel != "" { + al.channelManager.DismissToolFeedback(ctx, ts.channel, ts.chatID, ts.opts.InboundContext) + } logger.InfoCF("agent", "Tool output satisfied delivery; ending turn without follow-up LLM", map[string]any{ "agent_id": ts.agent.ID, diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index d56c4fd9b..c6dcfebe3 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -192,6 +192,21 @@ func clearTrackedToolFeedbackMessage( } } +// DismissToolFeedback clears any tracked tool feedback animation for the +// given channel/chat. This is called when a turn ends without a final +// response (e.g., ResponseHandled tools) to stop orphaned animation goroutines. +// outboundCtx carries topic/thread info for channels that use scoped tracker +// keys (e.g., Telegram forum topics); may be nil for non-topic channels. +func (m *Manager) DismissToolFeedback( + ctx context.Context, channelName, chatID string, outboundCtx *bus.InboundContext, +) { + ch, ok := m.GetChannel(channelName) + if !ok { + return + } + dismissTrackedToolFeedbackMessage(ctx, ch, chatID, outboundCtx) +} + func prepareToolFeedbackMessageContent(ch Channel, content string) string { prepared := strings.TrimSpace(content) if prepared == "" {