From 6bfbab2384418866caf856cc24126c38e35dd459 Mon Sep 17 00:00:00 2001 From: Alix-007 <267018309+Alix-007@users.noreply.github.com> Date: Sun, 29 Mar 2026 20:52:02 +0800 Subject: [PATCH] agent: keep thread metadata out of session history --- pkg/agent/loop.go | 48 ++++++++++++++++++++++++++++++++---------- pkg/agent/loop_test.go | 12 +++++------ 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index cfab995d5..79d746af0 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -1476,15 +1476,6 @@ func (al *AgentLoop) runAgentLoop( agent *AgentInstance, opts processOptions, ) (string, error) { - opts.UserMessage = formatUserMessageWithThreadMetadata( - opts.UserMessage, - opts.SenderDisplayName, - opts.SenderUsername, - opts.SenderID, - opts.MessageID, - opts.ReplyToMessageID, - ) - // Record last channel for heartbeat notifications (skip internal channels and cli) if opts.Channel != "" && opts.ChatID != "" && !constants.IsInternalChannel(opts.Channel) { channelKey := fmt.Sprintf("%s:%s", opts.Channel, opts.ChatID) @@ -1638,6 +1629,14 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState) (turnResult, er summary = ts.agent.Sessions.GetSummary(ts.sessionKey) } ts.captureRestorePoint(history, summary) + annotatedUserMessage := formatUserMessageWithThreadMetadata( + ts.userMessage, + ts.opts.SenderDisplayName, + ts.opts.SenderUsername, + ts.opts.SenderID, + ts.opts.MessageID, + ts.opts.ReplyToMessageID, + ) messages := ts.agent.ContextBuilder.BuildMessages( history, @@ -1650,6 +1649,7 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState) (turnResult, er ts.opts.SenderDisplayName, activeSkillNames(ts.agent, ts.opts)..., ) + annotateCurrentUserMessageForLLM(messages, ts.userMessage, annotatedUserMessage) cfg := al.GetConfig() maxMediaSize := cfg.Agents.Defaults.GetMaxMediaSize() @@ -1680,6 +1680,7 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState) (turnResult, er ts.opts.SenderID, ts.opts.SenderDisplayName, activeSkillNames(ts.agent, ts.opts)..., ) + annotateCurrentUserMessageForLLM(messages, ts.userMessage, annotatedUserMessage) messages = resolveMediaRefs(messages, al.mediaStore, maxMediaSize) } } @@ -2052,6 +2053,7 @@ turnLoop: nil, ts.channel, ts.chatID, ts.opts.SenderID, ts.opts.SenderDisplayName, activeSkillNames(ts.agent, ts.opts)..., ) + annotateCurrentUserMessageForLLM(messages, ts.userMessage, annotatedUserMessage) callMessages = messages if gracefulTerminal { callMessages = append(append([]providers.Message(nil), messages...), ts.interruptHintMessage()) @@ -3555,6 +3557,26 @@ func inboundMetadata(msg bus.InboundMessage, key string) string { return msg.Metadata[key] } +func annotateCurrentUserMessageForLLM( + messages []providers.Message, + rawUserMessage string, + annotatedUserMessage string, +) { + if rawUserMessage == annotatedUserMessage { + return + } + for i := len(messages) - 1; i >= 0; i-- { + if messages[i].Role != "user" { + continue + } + if messages[i].Content != rawUserMessage { + continue + } + messages[i].Content = annotatedUserMessage + return + } +} + func formatUserMessageWithThreadMetadata( content string, senderDisplayName string, @@ -3574,11 +3596,15 @@ func formatUserMessageWithThreadMetadata( if from != "" { metaParts = append(metaParts, fmt.Sprintf("from:%s", from)) } + msgMetaParts := make([]string, 0, 2) if messageID != "" { - metaParts = append(metaParts, fmt.Sprintf("msg:#%s", messageID)) + msgMetaParts = append(msgMetaParts, fmt.Sprintf("#%s", messageID)) } if replyToMessageID != "" { - metaParts = append(metaParts, fmt.Sprintf("reply_to:#%s", replyToMessageID)) + msgMetaParts = append(msgMetaParts, fmt.Sprintf("reply_to:#%s", replyToMessageID)) + } + if len(msgMetaParts) > 0 { + metaParts = append(metaParts, fmt.Sprintf("msgs:%s", strings.Join(msgMetaParts, ", "))) } annotation := fmt.Sprintf("[%s]", strings.Join(metaParts, "; ")) diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index db0b90d38..0cc616ccd 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -178,7 +178,7 @@ func TestFormatUserMessageWithThreadMetadata(t *testing.T) { t.Run("includes from, message and reply IDs", func(t *testing.T) { got := formatUserMessageWithThreadMetadata("ping", "Alice", "@alice", "discord:1", "123", "120") - want := "[from:Alice (@alice); msg:#123; reply_to:#120]\nping" + want := "[from:Alice (@alice); msgs:#123, reply_to:#120]\nping" if got != want { t.Fatalf("formatUserMessageWithThreadMetadata() = %q, want %q", got, want) } @@ -186,14 +186,14 @@ func TestFormatUserMessageWithThreadMetadata(t *testing.T) { t.Run("empty content returns annotation only", func(t *testing.T) { got := formatUserMessageWithThreadMetadata("", "", "", "discord:1", "123", "") - want := "[from:discord:1; msg:#123]" + want := "[from:discord:1; msgs:#123]" if got != want { t.Fatalf("formatUserMessageWithThreadMetadata() = %q, want %q", got, want) } }) } -func TestProcessMessage_AnnotatesThreadMetadataInPromptAndHistory(t *testing.T) { +func TestProcessMessage_AnnotatesThreadMetadataInPromptOnly(t *testing.T) { tmpDir, err := os.MkdirTemp("", "agent-test-*") if err != nil { t.Fatalf("Failed to create temp dir: %v", err) @@ -245,7 +245,7 @@ func TestProcessMessage_AnnotatesThreadMetadataInPromptAndHistory(t *testing.T) t.Fatal("provider did not receive any messages") } - wantAnnotated := "[from:Alice (@alice); msg:#123; reply_to:#120]\nhello" + wantAnnotated := "[from:Alice (@alice); msgs:#123, reply_to:#120]\nhello" lastMessage := provider.lastMessages[len(provider.lastMessages)-1] if lastMessage.Role != "user" || lastMessage.Content != wantAnnotated { t.Fatalf("last provider message = %+v, want user annotation %q", lastMessage, wantAnnotated) @@ -263,8 +263,8 @@ func TestProcessMessage_AnnotatesThreadMetadataInPromptAndHistory(t *testing.T) if len(history) != 2 { t.Fatalf("expected history len=2, got %d", len(history)) } - if history[0].Role != "user" || history[0].Content != wantAnnotated { - t.Fatalf("history user message = %+v, want %q", history[0], wantAnnotated) + if history[0].Role != "user" || history[0].Content != "hello" { + t.Fatalf("history user message = %+v, want %q", history[0], "hello") } }