Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions agent/compaction/compaction_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -696,3 +696,45 @@ func messageTexts(messages []*message.Message) []string {
}
return texts
}

func TestNewProvider_KeepsRetainedHistorySourceAcrossTurns(t *testing.T) {
// A compaction provider generates only summary messages; genuine prior-turn
// history returned from the persisted index must keep its original Source.
// Across turns the index is rebuilt from persisted groups whose message
// pointers differ from this turn's input, so identity-based attribution
// wrongly stamps real history as context-provider generated.
provider := compaction.NewContextProvider(compaction.ContextProviderConfig{
Strategy: &compaction.TruncationStrategy{Trigger: compaction.Never()},
SourceID: "compaction-test",
})
session := agenttest.CreateSession()

if _, _, err := invokeProvider(provider, t.Context(), []*message.Message{
textMessage(message.RoleUser, "u1"),
textMessage(message.RoleAssistant, "a1"),
}, agent.WithSession(session)); err != nil {
t.Fatalf("turn 1: %v", err)
}

out, _, err := invokeProvider(provider, t.Context(), []*message.Message{
textMessage(message.RoleUser, "u1"),
textMessage(message.RoleAssistant, "a1"),
textMessage(message.RoleUser, "u2"),
textMessage(message.RoleAssistant, "a2"),
}, agent.WithSession(session))
if err != nil {
t.Fatalf("turn 2: %v", err)
}

cp := message.Source{Type: agent.SourceTypeContextProvider, ID: "compaction-test"}
for i, msg := range out {
if msg.Source == cp {
t.Errorf("message %d (%q) mislabeled as context-provider generated; genuine history must not be attributed to the provider", i, msg.String())
}
}
// Retained prior-turn history restored from compaction state is attributed as
// chat history (matching .NET), not context-provider generated.
if len(out) < 2 || out[0].Source.Type != agent.SourceTypeHistoryProvider || out[1].Source.Type != agent.SourceTypeHistoryProvider {
t.Errorf("retained history should be marked as chat history, got %#v and %#v", out[0].Source, out[1].Source)
}
}
41 changes: 35 additions & 6 deletions agent/compaction/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ func (p *contextProvider) Invoking(ctx context.Context, invoking agent.InvokingC
var index *MessageIndex
if len(state.MessageGroups) > 0 {
index = NewMessageIndex(state.MessageGroups, p.tokenCounter)
// Treat every message restored from persisted compaction state as chat
// history before folding in this turn's input (matching .NET). This keeps
// markGeneratedMessages from re-attributing restored history and prevents
// those messages from being re-stored as new messages at the end of the run.
p.markRestoredMessagesAsHistory(index)
index.Update(messages)
} else {
index = CreateMessageIndex(messages, p.tokenCounter)
Expand Down Expand Up @@ -128,16 +133,17 @@ func (p *contextProvider) markGeneratedMessages(messages, inputMessages []*messa
if len(messages) == 0 {
return messages
}
originals := make(map[*message.Message]struct{}, len(inputMessages))
for _, msg := range inputMessages {
originals[msg] = struct{}{}
}
source := message.Source{Type: agent.SourceTypeContextProvider, ID: p.sourceID}
for i, msg := range messages {
if _, ok := originals[msg]; ok {
if msg == nil || msg.Source == source || msg.Source.Type == agent.SourceTypeHistoryProvider {
continue
}
if msg == nil || msg.Source == source {
// A message is provider-generated only when it is not one of this turn's
// input messages. Compare by content, not pointer identity: with a session
// the index is rebuilt from persisted groups whose message pointers differ
// from the incoming messages, so an identity check would wrongly stamp
// genuine prior-turn history as context-provider generated.
if containsMessageByContent(inputMessages, msg) {
continue
Comment thread
qmuntal marked this conversation as resolved.
}
marked := msg.Clone()
Expand All @@ -146,3 +152,26 @@ func (p *contextProvider) markGeneratedMessages(messages, inputMessages []*messa
}
return messages
}

// markRestoredMessagesAsHistory stamps every message restored from persisted
// compaction state with the history source so it is not later re-attributed as
// provider-generated or re-stored as a new message.
func (p *contextProvider) markRestoredMessagesAsHistory(index *MessageIndex) {
source := message.Source{Type: agent.SourceTypeHistoryProvider, ID: p.sourceID}
for _, group := range index.Groups {
for _, msg := range group.Messages {
if msg != nil {
msg.Source = source
}
}
}
}

func containsMessageByContent(messages []*message.Message, target *message.Message) bool {
for _, candidate := range messages {
if messageContentEqual(candidate, target) {
return true
}
}
return false
}
Loading