fix(message): suppress duplicate sends within same round

This commit is contained in:
mosir 2026-02-28 11:26:22 +08:00
parent c4080436db
commit a49803ec90
2 changed files with 50 additions and 0 deletions

View file

@ -12,6 +12,9 @@ type MessageTool struct {
defaultChannel string defaultChannel string
defaultChatID string defaultChatID string
sentInRound bool // Tracks whether a message was sent in the current processing round sentInRound bool // Tracks whether a message was sent in the current processing round
lastChannel string
lastChatID string
lastContent string
} }
func NewMessageTool() *MessageTool { func NewMessageTool() *MessageTool {
@ -51,6 +54,9 @@ func (t *MessageTool) SetContext(channel, chatID string) {
t.defaultChannel = channel t.defaultChannel = channel
t.defaultChatID = chatID t.defaultChatID = chatID
t.sentInRound = false // Reset send tracking for new processing round t.sentInRound = false // Reset send tracking for new processing round
t.lastChannel = ""
t.lastChatID = ""
t.lastContent = ""
} }
// HasSentInRound returns true if the message tool sent a message during the current round. // HasSentInRound returns true if the message tool sent a message during the current round.
@ -86,6 +92,15 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]any) *ToolRes
return &ToolResult{ForLLM: "Message sending not configured", IsError: true} return &ToolResult{ForLLM: "Message sending not configured", IsError: true}
} }
// Guard against accidental repeated tool-calls in one LLM round.
// If the same target and content were already sent, suppress duplicate delivery.
if t.sentInRound && t.lastChannel == channel && t.lastChatID == chatID && t.lastContent == content {
return &ToolResult{
ForLLM: "Duplicate message suppressed in current round",
Silent: true,
}
}
if err := t.sendCallback(channel, chatID, content); err != nil { if err := t.sendCallback(channel, chatID, content); err != nil {
return &ToolResult{ return &ToolResult{
ForLLM: fmt.Sprintf("sending message: %v", err), ForLLM: fmt.Sprintf("sending message: %v", err),
@ -95,6 +110,9 @@ func (t *MessageTool) Execute(ctx context.Context, args map[string]any) *ToolRes
} }
t.sentInRound = true t.sentInRound = true
t.lastChannel = channel
t.lastChatID = chatID
t.lastContent = content
// Silent: user already received the message directly // Silent: user already received the message directly
return &ToolResult{ return &ToolResult{
ForLLM: fmt.Sprintf("Message sent to %s:%s", channel, chatID), ForLLM: fmt.Sprintf("Message sent to %s:%s", channel, chatID),

View file

@ -194,6 +194,38 @@ func TestMessageTool_Execute_NotConfigured(t *testing.T) {
} }
} }
func TestMessageTool_Execute_SuppressDuplicateInSameRound(t *testing.T) {
tool := NewMessageTool()
tool.SetContext("test-channel", "test-chat-id")
callCount := 0
tool.SetSendCallback(func(channel, chatID, content string) error {
callCount++
return nil
})
ctx := context.Background()
args := map[string]any{
"content": "same message",
}
first := tool.Execute(ctx, args)
second := tool.Execute(ctx, args)
if callCount != 1 {
t.Fatalf("send callback call count = %d, want 1", callCount)
}
if !first.Silent || first.IsError {
t.Fatalf("first result unexpected: %+v", first)
}
if !second.Silent || second.IsError {
t.Fatalf("second result unexpected: %+v", second)
}
if second.ForLLM != "Duplicate message suppressed in current round" {
t.Fatalf("second ForLLM = %q", second.ForLLM)
}
}
func TestMessageTool_Name(t *testing.T) { func TestMessageTool_Name(t *testing.T) {
tool := NewMessageTool() tool := NewMessageTool()
if tool.Name() != "message" { if tool.Name() != "message" {