Skip to content

Commit ff07474

Browse files
google-genai-botcopybara-github
authored andcommitted
fix: deep-merge stateDelta maps when merging EventActions
PiperOrigin-RevId: 874480430
1 parent 2c9d4dd commit ff07474

2 files changed

Lines changed: 65 additions & 1 deletion

File tree

core/src/main/java/com/google/adk/events/EventActions.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.google.adk.sessions.State;
2323
import com.google.errorprone.annotations.CanIgnoreReturnValue;
2424
import java.util.HashSet;
25+
import java.util.Map;
2526
import java.util.Objects;
2627
import java.util.Optional;
2728
import java.util.Set;
@@ -383,7 +384,7 @@ public Builder compaction(EventCompaction value) {
383384
@CanIgnoreReturnValue
384385
public Builder merge(EventActions other) {
385386
other.skipSummarization().ifPresent(this::skipSummarization);
386-
this.stateDelta.putAll(other.stateDelta());
387+
other.stateDelta().forEach((key, value) -> stateDelta.merge(key, value, Builder::deepMerge));
387388
this.artifactDelta.putAll(other.artifactDelta());
388389
this.deletedArtifactIds.addAll(other.deletedArtifactIds());
389390
other.transferToAgent().ifPresent(this::transferToAgent);
@@ -395,6 +396,34 @@ public Builder merge(EventActions other) {
395396
return this;
396397
}
397398

399+
private static Object deepMerge(Object target, Object source) {
400+
if (!(target instanceof Map) || !(source instanceof Map)) {
401+
// If one of them is not a map, the source value overwrites the target.
402+
return source;
403+
}
404+
405+
Map<?, ?> targetMap = (Map<?, ?>) target;
406+
Map<?, ?> sourceMap = (Map<?, ?>) source;
407+
408+
if (!targetMap.isEmpty() && !sourceMap.isEmpty()) {
409+
Object targetKey = targetMap.keySet().iterator().next();
410+
Object sourceKey = sourceMap.keySet().iterator().next();
411+
if (targetKey != null
412+
&& sourceKey != null
413+
&& !targetKey.getClass().equals(sourceKey.getClass())) {
414+
throw new IllegalArgumentException(
415+
String.format(
416+
"Cannot merge maps with different key types: %s vs %s",
417+
targetKey.getClass().getName(), sourceKey.getClass().getName()));
418+
}
419+
}
420+
421+
// Create a new map to prevent UnsupportedOperationException from immutable maps
422+
Map<Object, Object> mergedMap = new ConcurrentHashMap<>(targetMap);
423+
sourceMap.forEach((key, value) -> mergedMap.merge(key, value, Builder::deepMerge));
424+
return mergedMap;
425+
}
426+
398427
public EventActions build() {
399428
return new EventActions(this);
400429
}

core/src/test/java/com/google/adk/events/EventActionsTest.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
package com.google.adk.events;
1818

1919
import static com.google.common.truth.Truth.assertThat;
20+
import static org.junit.Assert.assertThrows;
2021

2122
import com.google.adk.sessions.State;
2223
import com.google.common.collect.ImmutableMap;
2324
import com.google.common.collect.ImmutableSet;
2425
import com.google.genai.types.Content;
2526
import com.google.genai.types.Part;
27+
import java.util.Map;
2628
import java.util.concurrent.ConcurrentHashMap;
2729
import org.junit.Test;
2830
import org.junit.runner.RunWith;
@@ -130,4 +132,37 @@ public void jsonSerialization_works() throws Exception {
130132
assertThat(deserialized).isEqualTo(eventActions);
131133
assertThat(deserialized.deletedArtifactIds()).containsExactly("d1", "d2");
132134
}
135+
136+
@Test
137+
@SuppressWarnings("unchecked") // the nested map is known to be Map<String, Object>
138+
public void merge_deeplyMergesStateDelta() {
139+
EventActions eventActions1 = EventActions.builder().build();
140+
eventActions1.stateDelta().put("a", 1);
141+
eventActions1.stateDelta().put("b", ImmutableMap.of("nested1", 10, "nested2", 20));
142+
eventActions1.stateDelta().put("c", 100);
143+
EventActions eventActions2 = EventActions.builder().build();
144+
eventActions2.stateDelta().put("a", 2);
145+
eventActions2.stateDelta().put("b", ImmutableMap.of("nested2", 22, "nested3", 30));
146+
eventActions2.stateDelta().put("d", 200);
147+
148+
EventActions merged = eventActions1.toBuilder().merge(eventActions2).build();
149+
150+
assertThat(merged.stateDelta().keySet()).containsExactly("a", "b", "c", "d");
151+
assertThat(merged.stateDelta()).containsEntry("a", 2);
152+
assertThat((Map<String, Object>) merged.stateDelta().get("b"))
153+
.containsExactly("nested1", 10, "nested2", 22, "nested3", 30);
154+
assertThat(merged.stateDelta()).containsEntry("c", 100);
155+
assertThat(merged.stateDelta()).containsEntry("d", 200);
156+
}
157+
158+
@Test
159+
public void merge_failsOnMismatchedKeyTypesNestedInStateDelta() {
160+
EventActions eventActions1 = EventActions.builder().build();
161+
eventActions1.stateDelta().put("nested", ImmutableMap.of("a", 1));
162+
EventActions eventActions2 = EventActions.builder().build();
163+
eventActions2.stateDelta().put("nested", ImmutableMap.of(1, 2));
164+
165+
assertThrows(
166+
IllegalArgumentException.class, () -> eventActions1.toBuilder().merge(eventActions2));
167+
}
133168
}

0 commit comments

Comments
 (0)