From bd40583e67cbb62892998ccbe3e41a37d40e721d Mon Sep 17 00:00:00 2001 From: PratikDhanave Date: Sun, 6 Sep 2026 10:25:11 +0530 Subject: [PATCH] Detect changes in hosted content types in compaction contentEqual contentEqual is the change-detection predicate MessageIndex.Update uses to locate the already-processed prefix. Its switch handled text/reasoning/data/ uri/error/function-call/function-result/hosted-file and returned true (equal) for everything else. So two messages differing only in an MCPServerToolCall/Result, CodeInterpreterToolCall/Result, or UsageContent were treated as equal, and Update would not rebuild - preserving stale group and exclusion state and mis-locating the prefix boundary. Add explicit equality cases for the hosted content types the framework produces (recursing into Outputs/Inputs via contentsEqual, DeepEqual for UsageDetails). The default stays 'equal' so a genuinely new, forward-compatible content type still does not force a rebuild. --- agent/compaction/equality.go | 22 ++++++++++++++++++++++ agent/compaction/equality_test.go | 6 ++++++ 2 files changed, 28 insertions(+) diff --git a/agent/compaction/equality.go b/agent/compaction/equality.go index f62a21c1..d56996f9 100644 --- a/agent/compaction/equality.go +++ b/agent/compaction/equality.go @@ -70,7 +70,29 @@ func contentEqual(left, right message.Content) bool { case *message.HostedFileContent: rightContent := right.(*message.HostedFileContent) return leftContent.FileID == rightContent.FileID && leftContent.MediaType == rightContent.MediaType && leftContent.Name == rightContent.Name + case *message.MCPServerToolCallContent: + rightContent := right.(*message.MCPServerToolCallContent) + return leftContent.CallID == rightContent.CallID && leftContent.Name == rightContent.Name && + leftContent.ServerName == rightContent.ServerName && leftContent.Arguments == rightContent.Arguments + case *message.MCPServerToolResultContent: + rightContent := right.(*message.MCPServerToolResultContent) + return leftContent.CallID == rightContent.CallID && leftContent.Name == rightContent.Name && + leftContent.ServerName == rightContent.ServerName && leftContent.Error == rightContent.Error && + contentsEqual(leftContent.Outputs, rightContent.Outputs) + case *message.CodeInterpreterToolCallContent: + rightContent := right.(*message.CodeInterpreterToolCallContent) + return leftContent.CallID == rightContent.CallID && contentsEqual(leftContent.Inputs, rightContent.Inputs) + case *message.CodeInterpreterToolResultContent: + rightContent := right.(*message.CodeInterpreterToolResultContent) + return leftContent.CallID == rightContent.CallID && contentsEqual(leftContent.Outputs, rightContent.Outputs) + case *message.UsageContent: + rightContent := right.(*message.UsageContent) + return reflect.DeepEqual(leftContent.Details, rightContent.Details) default: + // An unhandled content type is treated as equal so a genuinely new, + // forward-compatible content type does not force an index rebuild. The + // hosted content types the framework produces are handled explicitly + // above so real changes in them are detected. return true } } diff --git a/agent/compaction/equality_test.go b/agent/compaction/equality_test.go index 346edaf5..95f7797f 100644 --- a/agent/compaction/equality_test.go +++ b/agent/compaction/equality_test.go @@ -68,6 +68,12 @@ func TestMessageIndexUpdate_MatchesKnownContentTypes(t *testing.T) { {name: "function result", original: &message.FunctionResultContent{CallID: "c1", Result: "sunny"}, replacement: &message.FunctionResultContent{CallID: "c1", Result: "rainy"}, matches: false}, {name: "function result error", original: &message.FunctionResultContent{CallID: "c1", Result: "sunny"}, replacement: &message.FunctionResultContent{CallID: "c1", Result: "sunny", Error: errors.New("boom")}, matches: false}, {name: "hosted file", original: &message.HostedFileContent{FileID: "file-1", MediaType: "text/csv", Name: "a.csv"}, replacement: &message.HostedFileContent{FileID: "file-1", MediaType: "text/csv", Name: "a.csv"}, matches: true}, + {name: "mcp call same", original: &message.MCPServerToolCallContent{CallID: "c1", Name: "fn", Arguments: `{"x":1}`}, replacement: &message.MCPServerToolCallContent{CallID: "c1", Name: "fn", Arguments: `{"x":1}`}, matches: true}, + {name: "mcp call args differ", original: &message.MCPServerToolCallContent{CallID: "c1", Name: "fn", Arguments: `{"x":1}`}, replacement: &message.MCPServerToolCallContent{CallID: "c1", Name: "fn", Arguments: `{"x":2}`}, matches: false}, + {name: "mcp result outputs differ", original: &message.MCPServerToolResultContent{CallID: "c1", Outputs: message.Contents{&message.TextContent{Text: "a"}}}, replacement: &message.MCPServerToolResultContent{CallID: "c1", Outputs: message.Contents{&message.TextContent{Text: "b"}}}, matches: false}, + {name: "code interpreter call inputs differ", original: &message.CodeInterpreterToolCallContent{CallID: "c1", Inputs: message.Contents{&message.TextContent{Text: "x=1"}}}, replacement: &message.CodeInterpreterToolCallContent{CallID: "c1", Inputs: message.Contents{&message.TextContent{Text: "x=2"}}}, matches: false}, + {name: "code interpreter result outputs differ", original: &message.CodeInterpreterToolResultContent{CallID: "c1", Outputs: message.Contents{&message.TextContent{Text: "1"}}}, replacement: &message.CodeInterpreterToolResultContent{CallID: "c1", Outputs: message.Contents{&message.TextContent{Text: "2"}}}, matches: false}, + {name: "usage differs", original: &message.UsageContent{Details: message.UsageDetails{InputTokenCount: 10}}, replacement: &message.UsageContent{Details: message.UsageDetails{InputTokenCount: 20}}, matches: false}, } for _, tt := range tests {