fix(channels): bypass placeholder edits for thought and tool calls

This commit is contained in:
lc6464
2026-04-26 19:43:25 +08:00
parent 9d42282672
commit bfc37b784e
2 changed files with 112 additions and 0 deletions
+14
View File
@@ -134,6 +134,14 @@ func outboundMessageIsToolFeedback(msg bus.OutboundMessage) bool {
return strings.EqualFold(strings.TrimSpace(msg.Context.Raw["message_kind"]), "tool_feedback")
}
func outboundMessageBypassesPlaceholderEdit(msg bus.OutboundMessage) bool {
if len(msg.Context.Raw) == 0 {
return false
}
kind := strings.TrimSpace(msg.Context.Raw["message_kind"])
return strings.EqualFold(kind, "thought") || strings.EqualFold(kind, "tool_calls")
}
func outboundMediaChannel(msg bus.OutboundMediaMessage) string {
return msg.Context.Channel
}
@@ -332,6 +340,12 @@ func (m *Manager) preSend(ctx context.Context, name string, msg bus.OutboundMess
}
return nil, false
}
if outboundMessageBypassesPlaceholderEdit(msg) {
if deleter, ok := ch.(MessageDeleter); ok {
deleter.DeleteMessage(ctx, chatID, entry.id) // best effort
}
return nil, false
}
if editor, ok := ch.(MessageEditor); ok {
content := msg.Content
trackedContent := msg.Content
+98
View File
@@ -1131,6 +1131,104 @@ func TestPreSend_ToolFeedbackSeparateMessagesDeletesPlaceholderAndSkipsEdit(t *t
}
}
func TestPreSend_ThoughtPlaceholderDeleteAndSkipsEdit(t *testing.T) {
m := newTestManager()
ch := &mockDeletingMessageEditor{
mockMessageEditor: mockMessageEditor{
editFn: func(_ context.Context, _, _, _ string) error {
t.Fatal("expected thought message to bypass placeholder edit")
return nil
},
},
}
m.RecordPlaceholder("test", "123", "456")
msg := testOutboundMessage(bus.OutboundMessage{
Channel: "test",
ChatID: "123",
Content: "thinking trace",
Context: bus.InboundContext{
Channel: "test",
ChatID: "123",
Raw: map[string]string{
"message_kind": "thought",
},
},
})
msgIDs, handled := m.preSend(context.Background(), "test", msg, ch)
if handled {
t.Fatalf("expected thought message to fall through so the channel can send a structured message, got %v", msgIDs)
}
if ch.deleteCalls != 1 {
t.Fatalf("expected placeholder deletion, got %d delete calls", ch.deleteCalls)
}
if ch.deletedChatID != "123" || ch.deletedMessageID != "456" {
t.Fatalf("unexpected placeholder deletion target: %s/%s", ch.deletedChatID, ch.deletedMessageID)
}
if _, ok := m.placeholders.Load("test:123"); ok {
t.Fatal("expected placeholder to be consumed before structured thought send")
}
}
func TestSendWithRetry_ToolCallsPlaceholderDeleteAndFallsThroughToSend(t *testing.T) {
m := newTestManager()
ch := &mockDeletingMessageEditor{
mockMessageEditor: mockMessageEditor{
mockChannel: mockChannel{
sendFn: func(_ context.Context, msg bus.OutboundMessage) error {
if got := msg.Context.Raw["message_kind"]; got != "tool_calls" {
t.Fatalf("expected tool_calls message kind, got %q", got)
}
if msg.Content != "" {
t.Fatalf("expected empty tool_calls content, got %q", msg.Content)
}
return nil
},
},
editFn: func(_ context.Context, _, _, _ string) error {
t.Fatal("expected tool_calls message to bypass placeholder edit")
return nil
},
},
}
m.RecordPlaceholder("test", "123", "456")
w := &channelWorker{
ch: ch,
limiter: rate.NewLimiter(rate.Inf, 1),
}
msg := testOutboundMessage(bus.OutboundMessage{
Channel: "test",
ChatID: "123",
Context: bus.InboundContext{
Channel: "test",
ChatID: "123",
Raw: map[string]string{
"message_kind": "tool_calls",
"tool_calls": `[{"tool_feedback_explanation":"Looking up config","function":{"name":"read_file","arguments":"{}"}}]`,
},
},
})
m.sendWithRetry(context.Background(), "test", w, msg)
if ch.deleteCalls != 1 {
t.Fatalf("expected placeholder deletion, got %d delete calls", ch.deleteCalls)
}
if ch.deletedChatID != "123" || ch.deletedMessageID != "456" {
t.Fatalf("unexpected placeholder deletion target: %s/%s", ch.deletedChatID, ch.deletedMessageID)
}
if len(ch.sentMessages) != 1 {
t.Fatalf("expected structured tool_calls message to be sent once, got %d", len(ch.sentMessages))
}
}
func TestPreSend_NonToolFeedbackSeparateMessagesClearsTrackedMessageWithoutDismiss(t *testing.T) {
m := newTestManager()
m.config = &config.Config{